@ranger-testing/ranger-cli 1.0.13 → 1.1.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 -183
- package/build/cli.js +219 -278
- package/build/cli.js.map +1 -1
- package/build/commands/addEnv.js +1 -1
- package/build/commands/addEnv.js.map +1 -1
- package/build/commands/authEncrypt.js +7 -6
- package/build/commands/authEncrypt.js.map +1 -1
- package/build/commands/clean.js +1 -1
- package/build/commands/clean.js.map +1 -1
- package/build/commands/config.js +5 -4
- package/build/commands/config.js.map +1 -1
- package/build/commands/dataMcpServer.js +1 -1
- package/build/commands/dataMcpServer.js.map +1 -1
- package/build/commands/env.js +17 -10
- package/build/commands/env.js.map +1 -1
- package/build/commands/feature.js +277 -285
- package/build/commands/feature.js.map +1 -1
- package/build/commands/hook.js +27 -0
- package/build/commands/hook.js.map +1 -0
- package/build/commands/hooks/disable.js +25 -0
- package/build/commands/hooks/disable.js.map +1 -0
- package/build/commands/hooks/enable.js +44 -0
- package/build/commands/hooks/enable.js.map +1 -0
- package/build/commands/hooks/exitPlanMode.js +35 -0
- package/build/commands/hooks/exitPlanMode.js.map +1 -0
- package/build/commands/hooks/index.js +10 -0
- package/build/commands/hooks/index.js.map +1 -0
- package/build/commands/hooks/output.js +53 -0
- package/build/commands/hooks/output.js.map +1 -0
- package/build/commands/hooks/planReminder.js +46 -0
- package/build/commands/hooks/planReminder.js.map +1 -0
- package/build/commands/hooks/planStart.js +30 -0
- package/build/commands/hooks/planStart.js.map +1 -0
- package/build/commands/hooks/postEdit.js +41 -0
- package/build/commands/hooks/postEdit.js.map +1 -0
- package/build/commands/hooks/preCompact.js +30 -0
- package/build/commands/hooks/preCompact.js.map +1 -0
- package/build/commands/hooks/sessionStart.js +35 -0
- package/build/commands/hooks/sessionStart.js.map +1 -0
- package/build/commands/hooks/stopHook.js +54 -0
- package/build/commands/hooks/stopHook.js.map +1 -0
- package/build/commands/index.js +1 -0
- package/build/commands/index.js.map +1 -1
- package/build/commands/skillup.js +41 -77
- package/build/commands/skillup.js.map +1 -1
- package/build/commands/start.js +1 -1
- package/build/commands/start.js.map +1 -1
- package/build/commands/status.js +47 -65
- package/build/commands/status.js.map +1 -1
- package/build/commands/update.js +32 -40
- package/build/commands/update.js.map +1 -1
- package/build/commands/updateEnv.js +1 -1
- package/build/commands/updateEnv.js.map +1 -1
- package/build/commands/useEnv.js +1 -1
- package/build/commands/useEnv.js.map +1 -1
- package/build/commands/utils/browserSessionsApi.js +1 -1
- package/build/commands/utils/browserSessionsApi.js.map +1 -1
- package/build/commands/utils/claudePlugin.js +85 -0
- package/build/commands/utils/claudePlugin.js.map +1 -0
- package/build/commands/utils/cliSecret.js +1 -1
- package/build/commands/utils/environment.js +0 -6
- package/build/commands/utils/environment.js.map +1 -1
- package/build/commands/utils/featureApi.js +82 -15
- package/build/commands/utils/featureApi.js.map +1 -1
- package/build/commands/utils/featureReportGenerator.js +37 -3
- package/build/commands/utils/featureReportGenerator.js.map +1 -1
- package/build/commands/utils/git.js +44 -0
- package/build/commands/utils/git.js.map +1 -0
- package/build/commands/utils/keychain.js +1 -1
- package/build/commands/utils/keychain.js.map +1 -1
- package/build/commands/utils/localAgentInstallationsApi.js +1 -1
- package/build/commands/utils/rangerRoot.js +30 -0
- package/build/commands/utils/rangerRoot.js.map +1 -0
- package/build/commands/utils/sessionCache.js +133 -0
- package/build/commands/utils/sessionCache.js.map +1 -0
- package/build/commands/utils/settings.js +7 -5
- package/build/commands/utils/settings.js.map +1 -1
- package/build/commands/utils/skillContent.js +28 -0
- package/build/commands/utils/skillContent.js.map +1 -0
- package/build/commands/utils/skills.js +1 -1
- package/build/commands/utils/skills.js.map +1 -1
- package/build/commands/utils/userApi.js +32 -0
- package/build/commands/utils/userApi.js.map +1 -0
- package/build/commands/verifyFeature.js +450 -105
- package/build/commands/verifyFeature.js.map +1 -1
- package/build/commands/verifyInBrowser.js +1 -1
- package/build/commands/verifyInBrowser.js.map +1 -1
- package/build/skills/bug-bash.md +31 -10
- package/build/skills/ranger/SKILL.md +164 -0
- package/build/skills/ranger/create.md +151 -0
- package/build/skills/ranger/start.md +122 -0
- package/build/skills/{feature-tracker → ranger}/verify.md +43 -17
- package/package.json +5 -3
- package/scripts/postinstall.js +18 -0
- package/build/commands/utils/mcpConfig.js +0 -1
- package/build/commands/utils/mcpConfig.js.map +0 -1
- package/build/skills/feature-tracker/SKILL.md +0 -185
- package/build/skills/feature-tracker/create.md +0 -105
- package/build/skills/feature-tracker/manage.md +0 -145
- package/build/skills/feature-tracker/report.md +0 -159
- package/build/skills/feature-tracker/start.md +0 -93
|
@@ -3,10 +3,11 @@ import { join } from 'path';
|
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
5
|
import inquirer from 'inquirer';
|
|
6
|
-
import { createFeature, listFeatures, getFeature, updateFeature,
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
import { createFeature, listFeatures, getFeature, updateFeature, getFeatureReportMarkdown, listFeatureSessions, startSession, concludeSession, createChecklistItem, } from './utils/featureApi.js';
|
|
7
|
+
import { getRangerDir } from './utils/rangerRoot.js';
|
|
8
|
+
import { registerSession } from './utils/sessionCache.js';
|
|
9
|
+
// Active feature file name (relative to .ranger dir)
|
|
10
|
+
const ACTIVE_FEATURE_FILE = 'active-feature.txt';
|
|
10
11
|
/**
|
|
11
12
|
* Get the current git remote URL
|
|
12
13
|
*/
|
|
@@ -44,7 +45,7 @@ function getGitBranch() {
|
|
|
44
45
|
* Get the active feature ID from the local file
|
|
45
46
|
*/
|
|
46
47
|
export async function getActiveFeatureId() {
|
|
47
|
-
const filePath = join(
|
|
48
|
+
const filePath = join(getRangerDir(), ACTIVE_FEATURE_FILE);
|
|
48
49
|
if (!existsSync(filePath)) {
|
|
49
50
|
return null;
|
|
50
51
|
}
|
|
@@ -60,24 +61,69 @@ export async function getActiveFeatureId() {
|
|
|
60
61
|
* Set the active feature ID
|
|
61
62
|
*/
|
|
62
63
|
async function setActiveFeatureId(featureId) {
|
|
63
|
-
const
|
|
64
|
-
const
|
|
64
|
+
const dir = getRangerDir();
|
|
65
|
+
const filePath = join(dir, ACTIVE_FEATURE_FILE);
|
|
65
66
|
if (!existsSync(dir)) {
|
|
66
67
|
await mkdir(dir, { recursive: true });
|
|
67
68
|
}
|
|
68
69
|
await writeFile(filePath, featureId, 'utf-8');
|
|
69
70
|
}
|
|
70
71
|
/**
|
|
71
|
-
*
|
|
72
|
+
* Get Dev Status display info for a feature based on completedAt and currentSession
|
|
73
|
+
*/
|
|
74
|
+
function getDevStatusDisplay(feature) {
|
|
75
|
+
if (feature.completedAt) {
|
|
76
|
+
return { emoji: '\u2705', label: 'Completed' };
|
|
77
|
+
}
|
|
78
|
+
if (!feature.currentSession) {
|
|
79
|
+
return { emoji: '\u2b1c', label: 'Unknown' };
|
|
80
|
+
}
|
|
81
|
+
switch (feature.currentSession.status) {
|
|
82
|
+
case 'ready':
|
|
83
|
+
return { emoji: '\u25b6\ufe0f', label: 'Ready' }; // ▶️
|
|
84
|
+
case 'in_progress':
|
|
85
|
+
return { emoji: '\ud83d\udd04', label: 'In Progress' }; // 🔄
|
|
86
|
+
case 'completed':
|
|
87
|
+
return { emoji: '\u2705', label: 'Completed' }; // session completed
|
|
88
|
+
default:
|
|
89
|
+
return { emoji: '\u2753', label: 'Unknown' };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get Review Status display info for a feature
|
|
94
|
+
*/
|
|
95
|
+
function getReviewStatusDisplay(feature) {
|
|
96
|
+
const { latestReview, currentSession, completedAt } = feature;
|
|
97
|
+
// Determine review status
|
|
98
|
+
if (completedAt) {
|
|
99
|
+
return { emoji: '\u2705', label: 'Approved' };
|
|
100
|
+
}
|
|
101
|
+
if (latestReview) {
|
|
102
|
+
if (!latestReview.submittedAt) {
|
|
103
|
+
return { emoji: '\ud83d\udcdd', label: 'In Review' }; // 📝
|
|
104
|
+
}
|
|
105
|
+
if (latestReview.overallVerdict === 'approved') {
|
|
106
|
+
return { emoji: '\u2705', label: 'Approved' };
|
|
107
|
+
}
|
|
108
|
+
return { emoji: '\ud83d\udd04', label: 'Changes Needed' }; // 🔄
|
|
109
|
+
}
|
|
110
|
+
if (currentSession?.status === 'completed') {
|
|
111
|
+
return { emoji: '\u23f3', label: 'Awaiting Review' }; // ⏳
|
|
112
|
+
}
|
|
113
|
+
// Not ready for review
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Display a feature with its checklist (current session items only)
|
|
72
118
|
*/
|
|
73
119
|
function displayFeature(feature, checklistItems) {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
120
|
+
const devStatus = getDevStatusDisplay(feature);
|
|
121
|
+
const reviewStatus = getReviewStatusDisplay(feature);
|
|
122
|
+
console.log(`\n${devStatus.emoji} ${feature.name} (${feature.dashboardUrl})`);
|
|
123
|
+
console.log(` Dev Status: ${devStatus.label}`);
|
|
124
|
+
if (reviewStatus) {
|
|
125
|
+
console.log(` Review Status: ${reviewStatus.emoji} ${reviewStatus.label}`);
|
|
126
|
+
}
|
|
81
127
|
if (feature.description) {
|
|
82
128
|
console.log(` Description: ${feature.description}`);
|
|
83
129
|
}
|
|
@@ -92,22 +138,30 @@ function displayFeature(feature, checklistItems) {
|
|
|
92
138
|
console.log(` Completed: ${new Date(feature.completedAt).toLocaleString()}`);
|
|
93
139
|
}
|
|
94
140
|
if (checklistItems && checklistItems.length > 0) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
141
|
+
// Filter to only show items from the current session
|
|
142
|
+
const currentSessionItems = feature.currentSessionId
|
|
143
|
+
? checklistItems.filter((item) => item.sessionId === feature.currentSessionId)
|
|
144
|
+
: checklistItems;
|
|
145
|
+
if (currentSessionItems.length > 0) {
|
|
146
|
+
console.log('\n Checklist:');
|
|
147
|
+
for (let i = 0; i < currentSessionItems.length; i++) {
|
|
148
|
+
const item = currentSessionItems[i];
|
|
149
|
+
const itemEmoji = item.status === 'verified'
|
|
150
|
+
? '\u2705'
|
|
151
|
+
: item.status === 'incomplete'
|
|
152
|
+
? '\ud83d\udfe0' // orange circle
|
|
153
|
+
: item.status === 'blocked'
|
|
154
|
+
? '\ud83d\uded1'
|
|
155
|
+
: item.status === 'canceled'
|
|
156
|
+
? '\u26d4'
|
|
157
|
+
: '\u2b1c'; // white square for pending
|
|
158
|
+
console.log(` ${i + 1}. ${itemEmoji} ${item.description}`);
|
|
159
|
+
if (item.status === 'blocked' && item.blockedReason) {
|
|
160
|
+
console.log(` Blocked: ${item.blockedReason}`);
|
|
161
|
+
}
|
|
162
|
+
if (item.status === 'canceled' && item.canceledReason) {
|
|
163
|
+
console.log(` Canceled: ${item.canceledReason}`);
|
|
164
|
+
}
|
|
111
165
|
}
|
|
112
166
|
}
|
|
113
167
|
}
|
|
@@ -116,9 +170,9 @@ function displayFeature(feature, checklistItems) {
|
|
|
116
170
|
* Create a new feature
|
|
117
171
|
*/
|
|
118
172
|
export async function featureCreate(name, options) {
|
|
119
|
-
//
|
|
173
|
+
// Get checklist items from array (each -c flag is one item)
|
|
120
174
|
const checklistItems = options.checklist
|
|
121
|
-
? options.checklist.
|
|
175
|
+
? options.checklist.map((s) => s.trim())
|
|
122
176
|
: [];
|
|
123
177
|
// Auto-detect git context
|
|
124
178
|
const gitRepoUrl = getGitRepoUrl();
|
|
@@ -133,6 +187,13 @@ export async function featureCreate(name, options) {
|
|
|
133
187
|
});
|
|
134
188
|
// Set as active feature
|
|
135
189
|
await setActiveFeatureId(feature.id);
|
|
190
|
+
// Start the session immediately so it's in_progress
|
|
191
|
+
await tryStartCurrentSession(feature.id);
|
|
192
|
+
// Register Claude session if provided via env var
|
|
193
|
+
const claudeSessionId = process.env.CLAUDE_SESSION_ID;
|
|
194
|
+
if (claudeSessionId) {
|
|
195
|
+
registerSession(claudeSessionId, feature.id);
|
|
196
|
+
}
|
|
136
197
|
console.log(`\n\u2705 Feature created: ${feature.id}`);
|
|
137
198
|
displayFeature(feature, feature.checklistItems);
|
|
138
199
|
console.log(`\n\u27a1\ufe0f Set as active feature`);
|
|
@@ -141,10 +202,10 @@ export async function featureCreate(name, options) {
|
|
|
141
202
|
* List features
|
|
142
203
|
*/
|
|
143
204
|
export async function featureList(options) {
|
|
144
|
-
const filters = {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
205
|
+
const filters = {
|
|
206
|
+
limit: options.limit ?? 10,
|
|
207
|
+
offset: options.offset ?? 0,
|
|
208
|
+
};
|
|
148
209
|
if (options.currentBranch) {
|
|
149
210
|
filters.gitRepoUrl = getGitRepoUrl();
|
|
150
211
|
filters.gitBranch = getGitBranch();
|
|
@@ -154,21 +215,32 @@ export async function featureList(options) {
|
|
|
154
215
|
console.log('\nNo features found.');
|
|
155
216
|
return;
|
|
156
217
|
}
|
|
157
|
-
|
|
218
|
+
const showing = result.totalCount > result.items.length
|
|
219
|
+
? `Showing ${result.items.length} of ${result.totalCount}`
|
|
220
|
+
: `${result.totalCount} feature(s) found`;
|
|
221
|
+
console.log(`\n${showing}:\n`);
|
|
158
222
|
for (const feature of result.items) {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
? '\ud83d\uded1'
|
|
163
|
-
: '\ud83d\udd04';
|
|
164
|
-
console.log(`${statusEmoji} ${feature.name}`);
|
|
223
|
+
const devStatus = getDevStatusDisplay(feature);
|
|
224
|
+
const reviewStatus = getReviewStatusDisplay(feature);
|
|
225
|
+
console.log(`${devStatus.emoji} ${feature.name}`);
|
|
165
226
|
console.log(` ID: ${feature.id}`);
|
|
166
|
-
console.log(` Status: ${
|
|
227
|
+
console.log(` Dev Status: ${devStatus.label}`);
|
|
228
|
+
if (reviewStatus) {
|
|
229
|
+
console.log(` Review Status: ${reviewStatus.emoji} ${reviewStatus.label}`);
|
|
230
|
+
}
|
|
167
231
|
if (feature.gitBranch) {
|
|
168
232
|
console.log(` Branch: ${feature.gitBranch}`);
|
|
169
233
|
}
|
|
170
234
|
console.log('');
|
|
171
235
|
}
|
|
236
|
+
// Show next page command if there are more items
|
|
237
|
+
const currentLimit = filters.limit ?? 10;
|
|
238
|
+
const nextOffset = (filters.offset ?? 0) + result.items.length;
|
|
239
|
+
if (nextOffset < result.totalCount) {
|
|
240
|
+
const limitFlag = currentLimit !== 10 ? ` --limit ${currentLimit}` : '';
|
|
241
|
+
const branchFlag = options.currentBranch ? ' --current-branch' : '';
|
|
242
|
+
console.log(`Next page: ranger feature list --offset ${nextOffset}${limitFlag}${branchFlag}`);
|
|
243
|
+
}
|
|
172
244
|
}
|
|
173
245
|
/**
|
|
174
246
|
* Show feature details
|
|
@@ -177,25 +249,38 @@ export async function featureShow(id) {
|
|
|
177
249
|
const featureId = id || (await getActiveFeatureId());
|
|
178
250
|
if (!featureId) {
|
|
179
251
|
console.error('\n\u274c No feature ID provided and no active feature set.');
|
|
180
|
-
console.error('Run: ranger feature
|
|
252
|
+
console.error('Run: ranger feature resume <id> to set an active feature');
|
|
181
253
|
process.exit(1);
|
|
182
254
|
}
|
|
183
255
|
const feature = await getFeature(featureId);
|
|
184
256
|
displayFeature(feature, feature.checklistItems);
|
|
185
257
|
}
|
|
186
|
-
/**
|
|
187
|
-
* Set active feature
|
|
188
|
-
*/
|
|
189
|
-
export async function featureUse(id) {
|
|
190
|
-
// Verify the feature exists
|
|
191
|
-
const feature = await getFeature(id);
|
|
192
|
-
await setActiveFeatureId(feature.id);
|
|
193
|
-
console.log(`\n\u2705 Active feature set to: ${feature.name} (${feature.id})`);
|
|
194
|
-
}
|
|
195
258
|
/**
|
|
196
259
|
* Resume feature matching current git context
|
|
260
|
+
* @param id Optional feature ID to resume directly (bypasses search/prompt)
|
|
197
261
|
*/
|
|
198
|
-
export async function featureResume() {
|
|
262
|
+
export async function featureResume(id) {
|
|
263
|
+
// If ID provided, set it as the active feature directly
|
|
264
|
+
if (id) {
|
|
265
|
+
const feature = await getFeature(id);
|
|
266
|
+
await setActiveFeatureId(feature.id);
|
|
267
|
+
// Update the feature's gitBranch to the current branch
|
|
268
|
+
const currentBranch = getGitBranch();
|
|
269
|
+
if (currentBranch && currentBranch !== feature.gitBranch) {
|
|
270
|
+
await updateFeature(feature.id, { gitBranch: currentBranch });
|
|
271
|
+
console.log(` Updated branch to: ${currentBranch}`);
|
|
272
|
+
}
|
|
273
|
+
console.log(`\n\u2705 Resumed feature: ${feature.name} (${feature.id})`);
|
|
274
|
+
// Start the current session if it's ready
|
|
275
|
+
await tryStartCurrentSession(feature.id);
|
|
276
|
+
// Register Claude session if provided via env var
|
|
277
|
+
const claudeSessionId = process.env.CLAUDE_SESSION_ID;
|
|
278
|
+
if (claudeSessionId) {
|
|
279
|
+
registerSession(claudeSessionId, feature.id);
|
|
280
|
+
}
|
|
281
|
+
displayFeature(feature, feature.checklistItems);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
199
284
|
const gitRepoUrl = getGitRepoUrl();
|
|
200
285
|
const gitBranch = getGitBranch();
|
|
201
286
|
if (!gitRepoUrl && !gitBranch) {
|
|
@@ -204,291 +289,198 @@ export async function featureResume() {
|
|
|
204
289
|
}
|
|
205
290
|
console.log(`\nSearching for features matching: ${gitRepoUrl || 'any'} / ${gitBranch || 'any'}...`);
|
|
206
291
|
const result = await listFeatures({
|
|
207
|
-
status: 'in_progress',
|
|
208
292
|
gitRepoUrl,
|
|
209
293
|
gitBranch,
|
|
210
294
|
});
|
|
211
|
-
|
|
212
|
-
|
|
295
|
+
// Filter to non-completed features
|
|
296
|
+
const activeFeatures = result.items.filter((f) => !f.completedAt);
|
|
297
|
+
if (activeFeatures.length === 0) {
|
|
298
|
+
console.log('\nNo matching active features found.');
|
|
213
299
|
console.log('Run: ranger feature create "<name>" to create a new feature');
|
|
214
300
|
return;
|
|
215
301
|
}
|
|
216
302
|
let selectedFeature;
|
|
217
|
-
if (
|
|
218
|
-
selectedFeature =
|
|
303
|
+
if (activeFeatures.length === 1) {
|
|
304
|
+
selectedFeature = activeFeatures[0];
|
|
219
305
|
}
|
|
220
306
|
else {
|
|
221
|
-
// Multiple matches -
|
|
307
|
+
// Multiple matches - check if we're in interactive mode
|
|
308
|
+
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
309
|
+
if (!isInteractive) {
|
|
310
|
+
// Non-interactive mode: list features and require explicit ID
|
|
311
|
+
console.log('\nNon-interactive mode detected. Multiple features found:');
|
|
312
|
+
for (const f of activeFeatures) {
|
|
313
|
+
const devStatus = getDevStatusDisplay(f);
|
|
314
|
+
console.log(` ${devStatus.emoji} ${f.name}`);
|
|
315
|
+
console.log(` ID: ${f.id}`);
|
|
316
|
+
if (f.gitBranch) {
|
|
317
|
+
console.log(` Branch: ${f.gitBranch}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
console.error('\nPlease specify the feature ID: ranger feature resume <id>');
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
// Interactive mode: prompt user to select
|
|
222
324
|
const { selected } = await inquirer.prompt([
|
|
223
325
|
{
|
|
224
326
|
type: 'list',
|
|
225
327
|
name: 'selected',
|
|
226
328
|
message: 'Multiple features found. Select one:',
|
|
227
|
-
choices:
|
|
329
|
+
choices: activeFeatures.map((f) => ({
|
|
228
330
|
name: `${f.name} (${f.id})`,
|
|
229
331
|
value: f.id,
|
|
230
332
|
})),
|
|
231
333
|
},
|
|
232
334
|
]);
|
|
233
|
-
selectedFeature =
|
|
335
|
+
selectedFeature = activeFeatures.find((f) => f.id === selected);
|
|
234
336
|
}
|
|
235
337
|
await setActiveFeatureId(selectedFeature.id);
|
|
338
|
+
// Update the feature's gitBranch to the current branch
|
|
339
|
+
if (gitBranch && gitBranch !== selectedFeature.gitBranch) {
|
|
340
|
+
await updateFeature(selectedFeature.id, { gitBranch });
|
|
341
|
+
console.log(` Updated branch to: ${gitBranch}`);
|
|
342
|
+
}
|
|
236
343
|
console.log(`\n\u2705 Resumed feature: ${selectedFeature.name} (${selectedFeature.id})`);
|
|
237
|
-
//
|
|
344
|
+
// Start the current session if it's ready
|
|
345
|
+
await tryStartCurrentSession(selectedFeature.id);
|
|
346
|
+
// Register Claude session if provided via env var
|
|
238
347
|
const fullFeature = await getFeature(selectedFeature.id);
|
|
348
|
+
const claudeSessionId = process.env.CLAUDE_SESSION_ID;
|
|
349
|
+
if (claudeSessionId) {
|
|
350
|
+
registerSession(claudeSessionId, fullFeature.id);
|
|
351
|
+
}
|
|
352
|
+
// Show current status
|
|
239
353
|
displayFeature(fullFeature, fullFeature.checklistItems);
|
|
240
354
|
}
|
|
241
355
|
/**
|
|
242
|
-
*
|
|
356
|
+
* Try to start the current session if it's in ready status
|
|
357
|
+
*/
|
|
358
|
+
async function tryStartCurrentSession(featureId) {
|
|
359
|
+
try {
|
|
360
|
+
const { sessions } = await listFeatureSessions(featureId);
|
|
361
|
+
// Find the current session (most recent ready one)
|
|
362
|
+
const readySession = sessions.find((s) => s.status === 'ready');
|
|
363
|
+
if (readySession) {
|
|
364
|
+
await startSession(featureId, readySession.id);
|
|
365
|
+
console.log(` \u25b6\ufe0f Started session ${readySession.iteration}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
// Silently ignore - session may already be started or in another state
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Generate feature report
|
|
243
374
|
*/
|
|
244
|
-
export async function
|
|
375
|
+
export async function featureReport(id, options) {
|
|
245
376
|
const featureId = id || (await getActiveFeatureId());
|
|
246
377
|
if (!featureId) {
|
|
247
378
|
console.error('\n\u274c No feature ID provided and no active feature set.');
|
|
248
379
|
process.exit(1);
|
|
249
380
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
if (
|
|
260
|
-
|
|
381
|
+
console.log('\nGenerating report...');
|
|
382
|
+
// Fetch markdown report from API (includes embedded screenshots)
|
|
383
|
+
const markdown = await getFeatureReportMarkdown(featureId, {
|
|
384
|
+
style: options.style,
|
|
385
|
+
});
|
|
386
|
+
// Determine output path
|
|
387
|
+
const outputPath = options.output || join(getRangerDir(), 'reports', `${featureId}.md`);
|
|
388
|
+
// Ensure directory exists
|
|
389
|
+
const outputDir = join(outputPath, '..');
|
|
390
|
+
if (!existsSync(outputDir)) {
|
|
391
|
+
await mkdir(outputDir, { recursive: true });
|
|
261
392
|
}
|
|
262
|
-
|
|
393
|
+
await writeFile(outputPath, markdown, 'utf-8');
|
|
394
|
+
console.log(`\n\u2705 Report generated: ${outputPath}`);
|
|
263
395
|
}
|
|
264
396
|
/**
|
|
265
|
-
*
|
|
397
|
+
* Conclude the current session (even with incomplete items)
|
|
266
398
|
*/
|
|
267
|
-
export async function
|
|
268
|
-
const featureId = await getActiveFeatureId();
|
|
399
|
+
export async function featureConcludeSession(id) {
|
|
400
|
+
const featureId = id || (await getActiveFeatureId());
|
|
269
401
|
if (!featureId) {
|
|
270
|
-
console.error('\n\u274c No active feature set.');
|
|
271
|
-
console.error('Run: ranger feature
|
|
402
|
+
console.error('\n\u274c No feature ID provided and no active feature set.');
|
|
403
|
+
console.error('Run: ranger feature resume <id> to set an active feature');
|
|
272
404
|
process.exit(1);
|
|
273
405
|
}
|
|
274
406
|
const feature = await getFeature(featureId);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
options.block !== undefined ||
|
|
279
|
-
options.cancel !== undefined ||
|
|
280
|
-
options.reset !== undefined;
|
|
281
|
-
// Non-interactive mode: handle flags directly
|
|
282
|
-
if (hasNonInteractiveFlag) {
|
|
283
|
-
// --add: Add a new item
|
|
284
|
-
if (options.add) {
|
|
285
|
-
const item = await addChecklistItem(featureId, {
|
|
286
|
-
description: options.add.trim(),
|
|
287
|
-
});
|
|
288
|
-
console.log(`\u2705 Added: ${item.description}`);
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
// --edit: Edit item description
|
|
292
|
-
if (options.edit !== undefined) {
|
|
293
|
-
if (!options.description) {
|
|
294
|
-
console.error('\u274c --description is required when using --edit');
|
|
295
|
-
process.exit(1);
|
|
296
|
-
}
|
|
297
|
-
const item = getItemByIndex(feature.checklistItems, options.edit);
|
|
298
|
-
await updateChecklistItem(featureId, item.id, {
|
|
299
|
-
description: options.description.trim(),
|
|
300
|
-
});
|
|
301
|
-
console.log(`\u270f\ufe0f Item ${options.edit} updated: ${options.description.trim()}`);
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
// --block: Block an item
|
|
305
|
-
if (options.block !== undefined) {
|
|
306
|
-
const item = getItemByIndex(feature.checklistItems, options.block);
|
|
307
|
-
await updateChecklistItem(featureId, item.id, {
|
|
308
|
-
status: 'blocked',
|
|
309
|
-
blockedReason: options.reason || undefined,
|
|
310
|
-
});
|
|
311
|
-
console.log(`\ud83d\uded1 Item ${options.block} blocked: ${item.description}`);
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
// --cancel: Cancel an item
|
|
315
|
-
if (options.cancel !== undefined) {
|
|
316
|
-
const item = getItemByIndex(feature.checklistItems, options.cancel);
|
|
317
|
-
await updateChecklistItem(featureId, item.id, {
|
|
318
|
-
status: 'canceled',
|
|
319
|
-
canceledReason: options.reason || undefined,
|
|
320
|
-
});
|
|
321
|
-
console.log(`\u26d4 Item ${options.cancel} canceled: ${item.description}`);
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
// --reset: Reset item to pending
|
|
325
|
-
if (options.reset !== undefined) {
|
|
326
|
-
const item = getItemByIndex(feature.checklistItems, options.reset);
|
|
327
|
-
await updateChecklistItem(featureId, item.id, {
|
|
328
|
-
status: 'pending',
|
|
329
|
-
});
|
|
330
|
-
console.log(`\u2b1c Item ${options.reset} reset to pending: ${item.description}`);
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
407
|
+
if (!feature.currentSessionId) {
|
|
408
|
+
console.error('\n\u274c No active session for this feature.');
|
|
409
|
+
process.exit(1);
|
|
333
410
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
for (let i = 0; i < feature.checklistItems.length; i++) {
|
|
340
|
-
const item = feature.checklistItems[i];
|
|
341
|
-
const itemEmoji = item.status === 'verified'
|
|
342
|
-
? '\u2705'
|
|
343
|
-
: item.status === 'blocked'
|
|
344
|
-
? '\ud83d\uded1'
|
|
345
|
-
: item.status === 'canceled'
|
|
346
|
-
? '\u26d4'
|
|
347
|
-
: '\u2b1c';
|
|
348
|
-
console.log(`${i + 1}. ${itemEmoji} ${item.description}`);
|
|
349
|
-
}
|
|
350
|
-
console.log('');
|
|
411
|
+
const { sessions } = await listFeatureSessions(featureId);
|
|
412
|
+
const currentSession = sessions.find((s) => s.id === feature.currentSessionId);
|
|
413
|
+
if (!currentSession) {
|
|
414
|
+
console.error('\n\u274c Current session not found.');
|
|
415
|
+
process.exit(1);
|
|
351
416
|
}
|
|
352
|
-
|
|
353
|
-
console.log('
|
|
354
|
-
}
|
|
355
|
-
// Prompt for action
|
|
356
|
-
const { action } = await inquirer.prompt([
|
|
357
|
-
{
|
|
358
|
-
type: 'list',
|
|
359
|
-
name: 'action',
|
|
360
|
-
message: 'What would you like to do?',
|
|
361
|
-
choices: [
|
|
362
|
-
{ name: 'Add new item', value: 'add' },
|
|
363
|
-
{ name: 'Update existing item', value: 'update' },
|
|
364
|
-
{ name: 'Cancel', value: 'cancel' },
|
|
365
|
-
],
|
|
366
|
-
},
|
|
367
|
-
]);
|
|
368
|
-
if (action === 'cancel') {
|
|
417
|
+
if (currentSession.status === 'completed') {
|
|
418
|
+
console.log('\n\u2705 Session is already completed.');
|
|
369
419
|
return;
|
|
370
420
|
}
|
|
371
|
-
if (
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
421
|
+
if (currentSession.status !== 'in_progress') {
|
|
422
|
+
console.error(`\n\u274c Cannot conclude session with status: ${currentSession.status}`);
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
console.log('\nConcluding session...');
|
|
426
|
+
await concludeSession(featureId, feature.currentSessionId);
|
|
427
|
+
console.log(`\n\u2705 Session ${currentSession.iteration} concluded.`);
|
|
428
|
+
console.log(` The feature is now ready for human review on the Ranger dashboard.`);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* List sessions for a feature
|
|
432
|
+
*/
|
|
433
|
+
export async function featureSessions(id) {
|
|
434
|
+
const featureId = id || (await getActiveFeatureId());
|
|
435
|
+
if (!featureId) {
|
|
436
|
+
console.error('\n\u274c No feature ID provided and no active feature set.');
|
|
437
|
+
process.exit(1);
|
|
438
|
+
}
|
|
439
|
+
const feature = await getFeature(featureId);
|
|
440
|
+
console.log(`\n\ud83d\udcc1 Sessions for: ${feature.name}\n`);
|
|
441
|
+
const { sessions } = await listFeatureSessions(featureId);
|
|
442
|
+
if (sessions.length === 0) {
|
|
443
|
+
console.log('No sessions found.');
|
|
384
444
|
return;
|
|
385
445
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
name: `${i + 1}. ${item.description}`,
|
|
398
|
-
value: i,
|
|
399
|
-
})),
|
|
400
|
-
},
|
|
401
|
-
]);
|
|
402
|
-
const selectedItem = feature.checklistItems[itemIndex];
|
|
403
|
-
const { updateAction } = await inquirer.prompt([
|
|
404
|
-
{
|
|
405
|
-
type: 'list',
|
|
406
|
-
name: 'updateAction',
|
|
407
|
-
message: 'What would you like to do with this item?',
|
|
408
|
-
choices: [
|
|
409
|
-
{ name: 'Edit description', value: 'edit' },
|
|
410
|
-
{ name: 'Mark as blocked', value: 'block' },
|
|
411
|
-
{ name: 'Cancel item', value: 'cancel' },
|
|
412
|
-
{ name: 'Reset to pending', value: 'pending' },
|
|
413
|
-
{ name: 'Go back', value: 'back' },
|
|
414
|
-
],
|
|
415
|
-
},
|
|
416
|
-
]);
|
|
417
|
-
if (updateAction === 'back') {
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
if (updateAction === 'edit') {
|
|
421
|
-
const { newDescription } = await inquirer.prompt([
|
|
422
|
-
{
|
|
423
|
-
type: 'input',
|
|
424
|
-
name: 'newDescription',
|
|
425
|
-
message: 'Enter new description:',
|
|
426
|
-
default: selectedItem.description,
|
|
427
|
-
validate: (input) => input.trim() ? true : 'Description is required',
|
|
428
|
-
},
|
|
429
|
-
]);
|
|
430
|
-
await updateChecklistItem(featureId, selectedItem.id, {
|
|
431
|
-
description: newDescription.trim(),
|
|
432
|
-
});
|
|
433
|
-
console.log(`\n\u270f\ufe0f Item updated: ${newDescription.trim()}`);
|
|
434
|
-
}
|
|
435
|
-
else if (updateAction === 'block') {
|
|
436
|
-
const { reason } = await inquirer.prompt([
|
|
437
|
-
{
|
|
438
|
-
type: 'input',
|
|
439
|
-
name: 'reason',
|
|
440
|
-
message: 'Enter blocking reason:',
|
|
441
|
-
},
|
|
442
|
-
]);
|
|
443
|
-
await updateChecklistItem(featureId, selectedItem.id, {
|
|
444
|
-
status: 'blocked',
|
|
445
|
-
blockedReason: reason || undefined,
|
|
446
|
-
});
|
|
447
|
-
console.log(`\n\ud83d\uded1 Item blocked: ${selectedItem.description}`);
|
|
448
|
-
}
|
|
449
|
-
else if (updateAction === 'cancel') {
|
|
450
|
-
const { reason } = await inquirer.prompt([
|
|
451
|
-
{
|
|
452
|
-
type: 'input',
|
|
453
|
-
name: 'reason',
|
|
454
|
-
message: 'Enter cancellation reason:',
|
|
455
|
-
},
|
|
456
|
-
]);
|
|
457
|
-
await updateChecklistItem(featureId, selectedItem.id, {
|
|
458
|
-
status: 'canceled',
|
|
459
|
-
canceledReason: reason || undefined,
|
|
460
|
-
});
|
|
461
|
-
console.log(`\n\u26d4 Item canceled: ${selectedItem.description}`);
|
|
462
|
-
}
|
|
463
|
-
else if (updateAction === 'pending') {
|
|
464
|
-
await updateChecklistItem(featureId, selectedItem.id, {
|
|
465
|
-
status: 'pending',
|
|
466
|
-
});
|
|
467
|
-
console.log(`\n\u2b1c Item reset to pending: ${selectedItem.description}`);
|
|
446
|
+
for (const session of sessions) {
|
|
447
|
+
const statusEmoji = session.status === 'completed'
|
|
448
|
+
? '\u2705'
|
|
449
|
+
: session.status === 'in_progress'
|
|
450
|
+
? '\ud83d\udd04'
|
|
451
|
+
: '\u25b6\ufe0f'; // ready
|
|
452
|
+
console.log(`${statusEmoji} Session ${session.iteration} (${session.id})`);
|
|
453
|
+
console.log(` Status: ${session.status}`);
|
|
454
|
+
console.log(` Created: ${new Date(session.createdAt).toLocaleString()}`);
|
|
455
|
+
if (session.completedAt) {
|
|
456
|
+
console.log(` Completed: ${new Date(session.completedAt).toLocaleString()}`);
|
|
468
457
|
}
|
|
458
|
+
console.log('');
|
|
469
459
|
}
|
|
470
460
|
}
|
|
471
461
|
/**
|
|
472
|
-
*
|
|
462
|
+
* Add a checklist item to the active feature
|
|
473
463
|
*/
|
|
474
|
-
export async function
|
|
464
|
+
export async function featureAddChecklistItem(description, id) {
|
|
475
465
|
const featureId = id || (await getActiveFeatureId());
|
|
476
466
|
if (!featureId) {
|
|
477
467
|
console.error('\n\u274c No feature ID provided and no active feature set.');
|
|
468
|
+
console.error('Run: ranger feature resume <id> to set an active feature');
|
|
478
469
|
process.exit(1);
|
|
479
470
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
const outputDir = join(outputPath, '..');
|
|
488
|
-
if (!existsSync(outputDir)) {
|
|
489
|
-
await mkdir(outputDir, { recursive: true });
|
|
471
|
+
const feature = await getFeature(featureId);
|
|
472
|
+
// Check if there's an active (non-submitted) review
|
|
473
|
+
if (feature.latestReview && !feature.latestReview.submittedAt) {
|
|
474
|
+
console.error('\n\u274c Cannot add checklist item: feature has an active review.');
|
|
475
|
+
console.error('Please finish your review first:');
|
|
476
|
+
console.error(` ${feature.dashboardUrl}`);
|
|
477
|
+
process.exit(1);
|
|
490
478
|
}
|
|
491
|
-
|
|
492
|
-
|
|
479
|
+
// Create the checklist item
|
|
480
|
+
await createChecklistItem(featureId, description);
|
|
481
|
+
// Refetch and display the updated feature
|
|
482
|
+
const updatedFeature = await getFeature(featureId);
|
|
483
|
+
console.log(`\n\u2705 Checklist item added`);
|
|
484
|
+
displayFeature(updatedFeature, updatedFeature.checklistItems);
|
|
493
485
|
}
|
|
494
486
|
//# sourceMappingURL=feature.js.map
|