@ranger-testing/ranger-cli 1.0.12 → 1.0.14
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 +28 -65
- package/build/cli.js +105 -102
- 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 +208 -273
- package/build/commands/feature.js.map +1 -1
- package/build/commands/index.js +3 -0
- package/build/commands/index.js.map +1 -1
- package/build/commands/postEditHook.js +25 -0
- package/build/commands/postEditHook.js.map +1 -0
- package/build/commands/preCompactHook.js +85 -0
- package/build/commands/preCompactHook.js.map +1 -0
- package/build/commands/sessionStartHook.js +64 -0
- package/build/commands/sessionStartHook.js.map +1 -0
- package/build/commands/skillup.js +21 -21
- 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 +30 -44
- 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/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 +68 -24
- 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/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/mcpConfig.js +1 -1
- package/build/commands/utils/rangerRoot.js +30 -0
- package/build/commands/utils/rangerRoot.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 +429 -104
- 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/feature-tracker/SKILL.md +8 -30
- package/build/skills/feature-tracker/create.md +47 -38
- package/build/skills/feature-tracker/start.md +4 -4
- package/build/skills/feature-tracker/verify.md +10 -14
- package/package.json +5 -3
- package/scripts/postinstall.js +18 -0
- package/build/skills/feature-tracker/manage.md +0 -145
|
@@ -3,10 +3,10 @@ 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,
|
|
7
|
-
import {
|
|
8
|
-
// Active feature file
|
|
9
|
-
const ACTIVE_FEATURE_FILE = '
|
|
6
|
+
import { createFeature, listFeatures, getFeature, getFeatureReportMarkdown, listFeatureSessions, startSession, concludeSession, } from './utils/featureApi.js';
|
|
7
|
+
import { getRangerDir } from './utils/rangerRoot.js';
|
|
8
|
+
// Active feature file name (relative to .ranger dir)
|
|
9
|
+
const ACTIVE_FEATURE_FILE = 'active-feature.txt';
|
|
10
10
|
/**
|
|
11
11
|
* Get the current git remote URL
|
|
12
12
|
*/
|
|
@@ -44,7 +44,7 @@ function getGitBranch() {
|
|
|
44
44
|
* Get the active feature ID from the local file
|
|
45
45
|
*/
|
|
46
46
|
export async function getActiveFeatureId() {
|
|
47
|
-
const filePath = join(
|
|
47
|
+
const filePath = join(getRangerDir(), ACTIVE_FEATURE_FILE);
|
|
48
48
|
if (!existsSync(filePath)) {
|
|
49
49
|
return null;
|
|
50
50
|
}
|
|
@@ -60,24 +60,77 @@ export async function getActiveFeatureId() {
|
|
|
60
60
|
* Set the active feature ID
|
|
61
61
|
*/
|
|
62
62
|
async function setActiveFeatureId(featureId) {
|
|
63
|
-
const
|
|
64
|
-
const
|
|
63
|
+
const dir = getRangerDir();
|
|
64
|
+
const filePath = join(dir, ACTIVE_FEATURE_FILE);
|
|
65
65
|
if (!existsSync(dir)) {
|
|
66
66
|
await mkdir(dir, { recursive: true });
|
|
67
67
|
}
|
|
68
68
|
await writeFile(filePath, featureId, 'utf-8');
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
|
-
*
|
|
71
|
+
* Get Dev Status display info for a feature based on completedAt and currentSession
|
|
72
|
+
*/
|
|
73
|
+
function getDevStatusDisplay(feature) {
|
|
74
|
+
if (feature.completedAt) {
|
|
75
|
+
return { emoji: '\u2705', label: 'Completed' };
|
|
76
|
+
}
|
|
77
|
+
if (!feature.currentSession) {
|
|
78
|
+
return { emoji: '\u2b1c', label: 'Unknown' };
|
|
79
|
+
}
|
|
80
|
+
switch (feature.currentSession.status) {
|
|
81
|
+
case 'ready':
|
|
82
|
+
return { emoji: '\u25b6\ufe0f', label: 'Ready' }; // ▶️
|
|
83
|
+
case 'in_progress':
|
|
84
|
+
return { emoji: '\ud83d\udd04', label: 'In Progress' }; // 🔄
|
|
85
|
+
case 'completed':
|
|
86
|
+
return { emoji: '\u2705', label: 'Completed' }; // session completed
|
|
87
|
+
default:
|
|
88
|
+
return { emoji: '\u2753', label: 'Unknown' };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get Review Status display info for a feature
|
|
93
|
+
*/
|
|
94
|
+
function getReviewStatusDisplay(feature) {
|
|
95
|
+
const { latestReview, currentSession, completedAt } = feature;
|
|
96
|
+
// Determine review status
|
|
97
|
+
if (completedAt) {
|
|
98
|
+
return { emoji: '\u2705', label: 'Approved' };
|
|
99
|
+
}
|
|
100
|
+
if (latestReview) {
|
|
101
|
+
if (!latestReview.submittedAt) {
|
|
102
|
+
return { emoji: '\ud83d\udcdd', label: 'In Review' }; // 📝
|
|
103
|
+
}
|
|
104
|
+
if (latestReview.overallVerdict === 'approved') {
|
|
105
|
+
return { emoji: '\u2705', label: 'Approved' };
|
|
106
|
+
}
|
|
107
|
+
return { emoji: '\ud83d\udd04', label: 'Changes Needed' }; // 🔄
|
|
108
|
+
}
|
|
109
|
+
if (currentSession?.status === 'completed') {
|
|
110
|
+
return { emoji: '\u23f3', label: 'Awaiting Review' }; // ⏳
|
|
111
|
+
}
|
|
112
|
+
// Not ready for review
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Display a feature with its checklist (current session items only)
|
|
72
117
|
*/
|
|
73
118
|
function displayFeature(feature, checklistItems) {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
119
|
+
const devStatus = getDevStatusDisplay(feature);
|
|
120
|
+
const reviewStatus = getReviewStatusDisplay(feature);
|
|
121
|
+
console.log(`\n${devStatus.emoji} ${feature.name} (${feature.id})`);
|
|
122
|
+
console.log(` Dev Status: ${devStatus.label}`);
|
|
123
|
+
if (reviewStatus) {
|
|
124
|
+
console.log(` Review Status: ${reviewStatus.emoji} ${reviewStatus.label}`);
|
|
125
|
+
}
|
|
126
|
+
if (feature.stats) {
|
|
127
|
+
const { verifiedItems, totalItems, blockedItems } = feature.stats;
|
|
128
|
+
let progressStr = ` Progress: ${verifiedItems}/${totalItems}`;
|
|
129
|
+
if (blockedItems > 0) {
|
|
130
|
+
progressStr += ` (${blockedItems} blocked)`;
|
|
131
|
+
}
|
|
132
|
+
console.log(progressStr);
|
|
133
|
+
}
|
|
81
134
|
if (feature.description) {
|
|
82
135
|
console.log(` Description: ${feature.description}`);
|
|
83
136
|
}
|
|
@@ -92,22 +145,30 @@ function displayFeature(feature, checklistItems) {
|
|
|
92
145
|
console.log(` Completed: ${new Date(feature.completedAt).toLocaleString()}`);
|
|
93
146
|
}
|
|
94
147
|
if (checklistItems && checklistItems.length > 0) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
148
|
+
// Filter to only show items from the current session
|
|
149
|
+
const currentSessionItems = feature.currentSessionId
|
|
150
|
+
? checklistItems.filter((item) => item.sessionId === feature.currentSessionId)
|
|
151
|
+
: checklistItems;
|
|
152
|
+
if (currentSessionItems.length > 0) {
|
|
153
|
+
console.log('\n Checklist:');
|
|
154
|
+
for (let i = 0; i < currentSessionItems.length; i++) {
|
|
155
|
+
const item = currentSessionItems[i];
|
|
156
|
+
const itemEmoji = item.status === 'verified'
|
|
157
|
+
? '\u2705'
|
|
158
|
+
: item.status === 'incomplete'
|
|
159
|
+
? '\ud83d\udfe0' // orange circle
|
|
160
|
+
: item.status === 'blocked'
|
|
161
|
+
? '\ud83d\uded1'
|
|
162
|
+
: item.status === 'canceled'
|
|
163
|
+
? '\u26d4'
|
|
164
|
+
: '\u2b1c'; // white square for pending
|
|
165
|
+
console.log(` ${i + 1}. ${itemEmoji} ${item.description}`);
|
|
166
|
+
if (item.status === 'blocked' && item.blockedReason) {
|
|
167
|
+
console.log(` Blocked: ${item.blockedReason}`);
|
|
168
|
+
}
|
|
169
|
+
if (item.status === 'canceled' && item.canceledReason) {
|
|
170
|
+
console.log(` Canceled: ${item.canceledReason}`);
|
|
171
|
+
}
|
|
111
172
|
}
|
|
112
173
|
}
|
|
113
174
|
}
|
|
@@ -116,9 +177,9 @@ function displayFeature(feature, checklistItems) {
|
|
|
116
177
|
* Create a new feature
|
|
117
178
|
*/
|
|
118
179
|
export async function featureCreate(name, options) {
|
|
119
|
-
//
|
|
180
|
+
// Get checklist items from array (each -c flag is one item)
|
|
120
181
|
const checklistItems = options.checklist
|
|
121
|
-
? options.checklist.
|
|
182
|
+
? options.checklist.map((s) => s.trim())
|
|
122
183
|
: [];
|
|
123
184
|
// Auto-detect git context
|
|
124
185
|
const gitRepoUrl = getGitRepoUrl();
|
|
@@ -133,6 +194,8 @@ export async function featureCreate(name, options) {
|
|
|
133
194
|
});
|
|
134
195
|
// Set as active feature
|
|
135
196
|
await setActiveFeatureId(feature.id);
|
|
197
|
+
// Start the session immediately so it's in_progress
|
|
198
|
+
await tryStartCurrentSession(feature.id);
|
|
136
199
|
console.log(`\n\u2705 Feature created: ${feature.id}`);
|
|
137
200
|
displayFeature(feature, feature.checklistItems);
|
|
138
201
|
console.log(`\n\u27a1\ufe0f Set as active feature`);
|
|
@@ -142,9 +205,6 @@ export async function featureCreate(name, options) {
|
|
|
142
205
|
*/
|
|
143
206
|
export async function featureList(options) {
|
|
144
207
|
const filters = {};
|
|
145
|
-
if (options.status) {
|
|
146
|
-
filters.status = options.status;
|
|
147
|
-
}
|
|
148
208
|
if (options.currentBranch) {
|
|
149
209
|
filters.gitRepoUrl = getGitRepoUrl();
|
|
150
210
|
filters.gitBranch = getGitBranch();
|
|
@@ -156,14 +216,22 @@ export async function featureList(options) {
|
|
|
156
216
|
}
|
|
157
217
|
console.log(`\n${result.items.length} feature(s) found:\n`);
|
|
158
218
|
for (const feature of result.items) {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
? '\ud83d\uded1'
|
|
163
|
-
: '\ud83d\udd04';
|
|
164
|
-
console.log(`${statusEmoji} ${feature.name}`);
|
|
219
|
+
const devStatus = getDevStatusDisplay(feature);
|
|
220
|
+
const reviewStatus = getReviewStatusDisplay(feature);
|
|
221
|
+
console.log(`${devStatus.emoji} ${feature.name}`);
|
|
165
222
|
console.log(` ID: ${feature.id}`);
|
|
166
|
-
console.log(` Status: ${
|
|
223
|
+
console.log(` Dev Status: ${devStatus.label}`);
|
|
224
|
+
if (reviewStatus) {
|
|
225
|
+
console.log(` Review Status: ${reviewStatus.emoji} ${reviewStatus.label}`);
|
|
226
|
+
}
|
|
227
|
+
if (feature.stats) {
|
|
228
|
+
const { verifiedItems, totalItems, blockedItems } = feature.stats;
|
|
229
|
+
let progressStr = ` Progress: ${verifiedItems}/${totalItems}`;
|
|
230
|
+
if (blockedItems > 0) {
|
|
231
|
+
progressStr += ` (${blockedItems} blocked)`;
|
|
232
|
+
}
|
|
233
|
+
console.log(progressStr);
|
|
234
|
+
}
|
|
167
235
|
if (feature.gitBranch) {
|
|
168
236
|
console.log(` Branch: ${feature.gitBranch}`);
|
|
169
237
|
}
|
|
@@ -194,8 +262,19 @@ export async function featureUse(id) {
|
|
|
194
262
|
}
|
|
195
263
|
/**
|
|
196
264
|
* Resume feature matching current git context
|
|
265
|
+
* @param id Optional feature ID to resume directly (bypasses search/prompt)
|
|
197
266
|
*/
|
|
198
|
-
export async function featureResume() {
|
|
267
|
+
export async function featureResume(id) {
|
|
268
|
+
// If ID provided, use it directly (like featureUse)
|
|
269
|
+
if (id) {
|
|
270
|
+
const feature = await getFeature(id);
|
|
271
|
+
await setActiveFeatureId(feature.id);
|
|
272
|
+
console.log(`\n\u2705 Resumed feature: ${feature.name} (${feature.id})`);
|
|
273
|
+
// Start the current session if it's ready
|
|
274
|
+
await tryStartCurrentSession(feature.id);
|
|
275
|
+
displayFeature(feature, feature.checklistItems);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
199
278
|
const gitRepoUrl = getGitRepoUrl();
|
|
200
279
|
const gitBranch = getGitBranch();
|
|
201
280
|
if (!gitRepoUrl && !gitBranch) {
|
|
@@ -204,18 +283,19 @@ export async function featureResume() {
|
|
|
204
283
|
}
|
|
205
284
|
console.log(`\nSearching for features matching: ${gitRepoUrl || 'any'} / ${gitBranch || 'any'}...`);
|
|
206
285
|
const result = await listFeatures({
|
|
207
|
-
status: 'in_progress',
|
|
208
286
|
gitRepoUrl,
|
|
209
287
|
gitBranch,
|
|
210
288
|
});
|
|
211
|
-
|
|
212
|
-
|
|
289
|
+
// Filter to non-completed features
|
|
290
|
+
const activeFeatures = result.items.filter((f) => !f.completedAt);
|
|
291
|
+
if (activeFeatures.length === 0) {
|
|
292
|
+
console.log('\nNo matching active features found.');
|
|
213
293
|
console.log('Run: ranger feature create "<name>" to create a new feature');
|
|
214
294
|
return;
|
|
215
295
|
}
|
|
216
296
|
let selectedFeature;
|
|
217
|
-
if (
|
|
218
|
-
selectedFeature =
|
|
297
|
+
if (activeFeatures.length === 1) {
|
|
298
|
+
selectedFeature = activeFeatures[0];
|
|
219
299
|
}
|
|
220
300
|
else {
|
|
221
301
|
// Multiple matches - prompt user to select
|
|
@@ -224,271 +304,126 @@ export async function featureResume() {
|
|
|
224
304
|
type: 'list',
|
|
225
305
|
name: 'selected',
|
|
226
306
|
message: 'Multiple features found. Select one:',
|
|
227
|
-
choices:
|
|
307
|
+
choices: activeFeatures.map((f) => ({
|
|
228
308
|
name: `${f.name} (${f.id})`,
|
|
229
309
|
value: f.id,
|
|
230
310
|
})),
|
|
231
311
|
},
|
|
232
312
|
]);
|
|
233
|
-
selectedFeature =
|
|
313
|
+
selectedFeature = activeFeatures.find((f) => f.id === selected);
|
|
234
314
|
}
|
|
235
315
|
await setActiveFeatureId(selectedFeature.id);
|
|
236
316
|
console.log(`\n\u2705 Resumed feature: ${selectedFeature.name} (${selectedFeature.id})`);
|
|
317
|
+
// Start the current session if it's ready
|
|
318
|
+
await tryStartCurrentSession(selectedFeature.id);
|
|
237
319
|
// Show current status
|
|
238
320
|
const fullFeature = await getFeature(selectedFeature.id);
|
|
239
321
|
displayFeature(fullFeature, fullFeature.checklistItems);
|
|
240
322
|
}
|
|
241
323
|
/**
|
|
242
|
-
*
|
|
324
|
+
* Try to start the current session if it's in ready status
|
|
243
325
|
*/
|
|
244
|
-
|
|
326
|
+
async function tryStartCurrentSession(featureId) {
|
|
327
|
+
try {
|
|
328
|
+
const { sessions } = await listFeatureSessions(featureId);
|
|
329
|
+
// Find the current session (most recent ready one)
|
|
330
|
+
const readySession = sessions.find((s) => s.status === 'ready');
|
|
331
|
+
if (readySession) {
|
|
332
|
+
await startSession(featureId, readySession.id);
|
|
333
|
+
console.log(` \u25b6\ufe0f Started session ${readySession.iteration}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
// Silently ignore - session may already be started or in another state
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Generate feature report
|
|
342
|
+
*/
|
|
343
|
+
export async function featureReport(id, options) {
|
|
245
344
|
const featureId = id || (await getActiveFeatureId());
|
|
246
345
|
if (!featureId) {
|
|
247
346
|
console.error('\n\u274c No feature ID provided and no active feature set.');
|
|
248
347
|
process.exit(1);
|
|
249
348
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
if (
|
|
260
|
-
|
|
349
|
+
console.log('\nGenerating report...');
|
|
350
|
+
// Fetch markdown report from API (includes embedded screenshots)
|
|
351
|
+
const markdown = await getFeatureReportMarkdown(featureId, {
|
|
352
|
+
style: options.style,
|
|
353
|
+
});
|
|
354
|
+
// Determine output path
|
|
355
|
+
const outputPath = options.output || join(getRangerDir(), 'reports', `${featureId}.md`);
|
|
356
|
+
// Ensure directory exists
|
|
357
|
+
const outputDir = join(outputPath, '..');
|
|
358
|
+
if (!existsSync(outputDir)) {
|
|
359
|
+
await mkdir(outputDir, { recursive: true });
|
|
261
360
|
}
|
|
262
|
-
|
|
361
|
+
await writeFile(outputPath, markdown, 'utf-8');
|
|
362
|
+
console.log(`\n\u2705 Report generated: ${outputPath}`);
|
|
263
363
|
}
|
|
264
364
|
/**
|
|
265
|
-
*
|
|
365
|
+
* Conclude the current session (even with incomplete items)
|
|
266
366
|
*/
|
|
267
|
-
export async function
|
|
268
|
-
const featureId = await getActiveFeatureId();
|
|
367
|
+
export async function featureConcludeSession(id) {
|
|
368
|
+
const featureId = id || (await getActiveFeatureId());
|
|
269
369
|
if (!featureId) {
|
|
270
|
-
console.error('\n\u274c No active feature set.');
|
|
370
|
+
console.error('\n\u274c No feature ID provided and no active feature set.');
|
|
271
371
|
console.error('Run: ranger feature use <id> to set an active feature');
|
|
272
372
|
process.exit(1);
|
|
273
373
|
}
|
|
274
374
|
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
|
-
}
|
|
333
|
-
}
|
|
334
|
-
// Interactive mode: show prompts
|
|
335
|
-
console.log(`\nManaging: ${feature.name} (${feature.id})\n`);
|
|
336
|
-
// Show current checklist
|
|
337
|
-
if (feature.checklistItems.length > 0) {
|
|
338
|
-
console.log('Current checklist:');
|
|
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('');
|
|
375
|
+
if (!feature.currentSessionId) {
|
|
376
|
+
console.error('\n\u274c No active session for this feature.');
|
|
377
|
+
process.exit(1);
|
|
351
378
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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') {
|
|
369
|
-
return;
|
|
379
|
+
const { sessions } = await listFeatureSessions(featureId);
|
|
380
|
+
const currentSession = sessions.find((s) => s.id === feature.currentSessionId);
|
|
381
|
+
if (!currentSession) {
|
|
382
|
+
console.error('\n\u274c Current session not found.');
|
|
383
|
+
process.exit(1);
|
|
370
384
|
}
|
|
371
|
-
if (
|
|
372
|
-
|
|
373
|
-
{
|
|
374
|
-
type: 'input',
|
|
375
|
-
name: 'description',
|
|
376
|
-
message: 'Enter item description:',
|
|
377
|
-
validate: (input) => input.trim() ? true : 'Description is required',
|
|
378
|
-
},
|
|
379
|
-
]);
|
|
380
|
-
const item = await addChecklistItem(featureId, {
|
|
381
|
-
description: description.trim(),
|
|
382
|
-
});
|
|
383
|
-
console.log(`\n\u2705 Added: ${item.description}`);
|
|
385
|
+
if (currentSession.status === 'completed') {
|
|
386
|
+
console.log('\n\u2705 Session is already completed.');
|
|
384
387
|
return;
|
|
385
388
|
}
|
|
386
|
-
if (
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
const { itemIndex } = await inquirer.prompt([
|
|
392
|
-
{
|
|
393
|
-
type: 'list',
|
|
394
|
-
name: 'itemIndex',
|
|
395
|
-
message: 'Select item to update:',
|
|
396
|
-
choices: feature.checklistItems.map((item, i) => ({
|
|
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}`);
|
|
468
|
-
}
|
|
389
|
+
if (currentSession.status !== 'in_progress') {
|
|
390
|
+
console.error(`\n\u274c Cannot conclude session with status: ${currentSession.status}`);
|
|
391
|
+
process.exit(1);
|
|
469
392
|
}
|
|
393
|
+
console.log('\nConcluding session...');
|
|
394
|
+
await concludeSession(featureId, feature.currentSessionId);
|
|
395
|
+
console.log(`\n\u2705 Session ${currentSession.iteration} concluded.`);
|
|
396
|
+
console.log(` The feature is now ready for human review on the Ranger dashboard.`);
|
|
470
397
|
}
|
|
471
398
|
/**
|
|
472
|
-
*
|
|
399
|
+
* List sessions for a feature
|
|
473
400
|
*/
|
|
474
|
-
export async function
|
|
401
|
+
export async function featureSessions(id) {
|
|
475
402
|
const featureId = id || (await getActiveFeatureId());
|
|
476
403
|
if (!featureId) {
|
|
477
404
|
console.error('\n\u274c No feature ID provided and no active feature set.');
|
|
478
405
|
process.exit(1);
|
|
479
406
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
407
|
+
const feature = await getFeature(featureId);
|
|
408
|
+
console.log(`\n\ud83d\udcc1 Sessions for: ${feature.name}\n`);
|
|
409
|
+
const { sessions } = await listFeatureSessions(featureId);
|
|
410
|
+
if (sessions.length === 0) {
|
|
411
|
+
console.log('No sessions found.');
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
for (const session of sessions) {
|
|
415
|
+
const statusEmoji = session.status === 'completed'
|
|
416
|
+
? '\u2705'
|
|
417
|
+
: session.status === 'in_progress'
|
|
418
|
+
? '\ud83d\udd04'
|
|
419
|
+
: '\u25b6\ufe0f'; // ready
|
|
420
|
+
console.log(`${statusEmoji} Session ${session.iteration} (${session.id})`);
|
|
421
|
+
console.log(` Status: ${session.status}`);
|
|
422
|
+
console.log(` Created: ${new Date(session.createdAt).toLocaleString()}`);
|
|
423
|
+
if (session.completedAt) {
|
|
424
|
+
console.log(` Completed: ${new Date(session.completedAt).toLocaleString()}`);
|
|
425
|
+
}
|
|
426
|
+
console.log('');
|
|
490
427
|
}
|
|
491
|
-
await writeFile(outputPath, markdown, 'utf-8');
|
|
492
|
-
console.log(`\n\u2705 Report generated: ${outputPath}`);
|
|
493
428
|
}
|
|
494
429
|
//# sourceMappingURL=feature.js.map
|