@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.
Files changed (101) hide show
  1. package/README.md +72 -183
  2. package/build/cli.js +219 -278
  3. package/build/cli.js.map +1 -1
  4. package/build/commands/addEnv.js +1 -1
  5. package/build/commands/addEnv.js.map +1 -1
  6. package/build/commands/authEncrypt.js +7 -6
  7. package/build/commands/authEncrypt.js.map +1 -1
  8. package/build/commands/clean.js +1 -1
  9. package/build/commands/clean.js.map +1 -1
  10. package/build/commands/config.js +5 -4
  11. package/build/commands/config.js.map +1 -1
  12. package/build/commands/dataMcpServer.js +1 -1
  13. package/build/commands/dataMcpServer.js.map +1 -1
  14. package/build/commands/env.js +17 -10
  15. package/build/commands/env.js.map +1 -1
  16. package/build/commands/feature.js +277 -285
  17. package/build/commands/feature.js.map +1 -1
  18. package/build/commands/hook.js +27 -0
  19. package/build/commands/hook.js.map +1 -0
  20. package/build/commands/hooks/disable.js +25 -0
  21. package/build/commands/hooks/disable.js.map +1 -0
  22. package/build/commands/hooks/enable.js +44 -0
  23. package/build/commands/hooks/enable.js.map +1 -0
  24. package/build/commands/hooks/exitPlanMode.js +35 -0
  25. package/build/commands/hooks/exitPlanMode.js.map +1 -0
  26. package/build/commands/hooks/index.js +10 -0
  27. package/build/commands/hooks/index.js.map +1 -0
  28. package/build/commands/hooks/output.js +53 -0
  29. package/build/commands/hooks/output.js.map +1 -0
  30. package/build/commands/hooks/planReminder.js +46 -0
  31. package/build/commands/hooks/planReminder.js.map +1 -0
  32. package/build/commands/hooks/planStart.js +30 -0
  33. package/build/commands/hooks/planStart.js.map +1 -0
  34. package/build/commands/hooks/postEdit.js +41 -0
  35. package/build/commands/hooks/postEdit.js.map +1 -0
  36. package/build/commands/hooks/preCompact.js +30 -0
  37. package/build/commands/hooks/preCompact.js.map +1 -0
  38. package/build/commands/hooks/sessionStart.js +35 -0
  39. package/build/commands/hooks/sessionStart.js.map +1 -0
  40. package/build/commands/hooks/stopHook.js +54 -0
  41. package/build/commands/hooks/stopHook.js.map +1 -0
  42. package/build/commands/index.js +1 -0
  43. package/build/commands/index.js.map +1 -1
  44. package/build/commands/skillup.js +41 -77
  45. package/build/commands/skillup.js.map +1 -1
  46. package/build/commands/start.js +1 -1
  47. package/build/commands/start.js.map +1 -1
  48. package/build/commands/status.js +47 -65
  49. package/build/commands/status.js.map +1 -1
  50. package/build/commands/update.js +32 -40
  51. package/build/commands/update.js.map +1 -1
  52. package/build/commands/updateEnv.js +1 -1
  53. package/build/commands/updateEnv.js.map +1 -1
  54. package/build/commands/useEnv.js +1 -1
  55. package/build/commands/useEnv.js.map +1 -1
  56. package/build/commands/utils/browserSessionsApi.js +1 -1
  57. package/build/commands/utils/browserSessionsApi.js.map +1 -1
  58. package/build/commands/utils/claudePlugin.js +85 -0
  59. package/build/commands/utils/claudePlugin.js.map +1 -0
  60. package/build/commands/utils/cliSecret.js +1 -1
  61. package/build/commands/utils/environment.js +0 -6
  62. package/build/commands/utils/environment.js.map +1 -1
  63. package/build/commands/utils/featureApi.js +82 -15
  64. package/build/commands/utils/featureApi.js.map +1 -1
  65. package/build/commands/utils/featureReportGenerator.js +37 -3
  66. package/build/commands/utils/featureReportGenerator.js.map +1 -1
  67. package/build/commands/utils/git.js +44 -0
  68. package/build/commands/utils/git.js.map +1 -0
  69. package/build/commands/utils/keychain.js +1 -1
  70. package/build/commands/utils/keychain.js.map +1 -1
  71. package/build/commands/utils/localAgentInstallationsApi.js +1 -1
  72. package/build/commands/utils/rangerRoot.js +30 -0
  73. package/build/commands/utils/rangerRoot.js.map +1 -0
  74. package/build/commands/utils/sessionCache.js +133 -0
  75. package/build/commands/utils/sessionCache.js.map +1 -0
  76. package/build/commands/utils/settings.js +7 -5
  77. package/build/commands/utils/settings.js.map +1 -1
  78. package/build/commands/utils/skillContent.js +28 -0
  79. package/build/commands/utils/skillContent.js.map +1 -0
  80. package/build/commands/utils/skills.js +1 -1
  81. package/build/commands/utils/skills.js.map +1 -1
  82. package/build/commands/utils/userApi.js +32 -0
  83. package/build/commands/utils/userApi.js.map +1 -0
  84. package/build/commands/verifyFeature.js +450 -105
  85. package/build/commands/verifyFeature.js.map +1 -1
  86. package/build/commands/verifyInBrowser.js +1 -1
  87. package/build/commands/verifyInBrowser.js.map +1 -1
  88. package/build/skills/bug-bash.md +31 -10
  89. package/build/skills/ranger/SKILL.md +164 -0
  90. package/build/skills/ranger/create.md +151 -0
  91. package/build/skills/ranger/start.md +122 -0
  92. package/build/skills/{feature-tracker → ranger}/verify.md +43 -17
  93. package/package.json +5 -3
  94. package/scripts/postinstall.js +18 -0
  95. package/build/commands/utils/mcpConfig.js +0 -1
  96. package/build/commands/utils/mcpConfig.js.map +0 -1
  97. package/build/skills/feature-tracker/SKILL.md +0 -185
  98. package/build/skills/feature-tracker/create.md +0 -105
  99. package/build/skills/feature-tracker/manage.md +0 -145
  100. package/build/skills/feature-tracker/report.md +0 -159
  101. 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, addChecklistItem, updateChecklistItem, getFeatureReport, } from './utils/featureApi.js';
7
- import { generateFeatureReport } from './utils/featureReportGenerator.js';
8
- // Active feature file path
9
- const ACTIVE_FEATURE_FILE = '.ranger/active-feature.txt';
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(process.cwd(), ACTIVE_FEATURE_FILE);
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 filePath = join(process.cwd(), ACTIVE_FEATURE_FILE);
64
- const dir = join(process.cwd(), '.ranger');
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
- * Display a feature with its checklist
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 statusEmoji = feature.status === 'completed'
75
- ? '\u2705'
76
- : feature.status === 'blocked'
77
- ? '\ud83d\uded1'
78
- : '\ud83d\udd04';
79
- console.log(`\n${statusEmoji} ${feature.name} (${feature.id})`);
80
- console.log(` Status: ${feature.status}`);
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
- console.log('\n Checklist:');
96
- for (let i = 0; i < checklistItems.length; i++) {
97
- const item = checklistItems[i];
98
- const itemEmoji = item.status === 'verified'
99
- ? '\u2705'
100
- : item.status === 'blocked'
101
- ? '\ud83d\uded1'
102
- : item.status === 'canceled'
103
- ? '\u26d4'
104
- : '\u2b1c'; // white square for pending
105
- console.log(` ${i + 1}. ${itemEmoji} ${item.description}`);
106
- if (item.status === 'blocked' && item.blockedReason) {
107
- console.log(` Blocked: ${item.blockedReason}`);
108
- }
109
- if (item.status === 'canceled' && item.canceledReason) {
110
- console.log(` Canceled: ${item.canceledReason}`);
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
- // Parse checklist from comma-separated string
173
+ // Get checklist items from array (each -c flag is one item)
120
174
  const checklistItems = options.checklist
121
- ? options.checklist.split(',').map((s) => s.trim())
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
- if (options.status) {
146
- filters.status = options.status;
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
- console.log(`\n${result.items.length} feature(s) found:\n`);
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 statusEmoji = feature.status === 'completed'
160
- ? '\u2705'
161
- : feature.status === 'blocked'
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: ${feature.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 use <id> to set an active 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
- if (result.items.length === 0) {
212
- console.log('\nNo matching in-progress features found.');
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 (result.items.length === 1) {
218
- selectedFeature = result.items[0];
303
+ if (activeFeatures.length === 1) {
304
+ selectedFeature = activeFeatures[0];
219
305
  }
220
306
  else {
221
- // Multiple matches - prompt user to select
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: result.items.map((f) => ({
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 = result.items.find((f) => f.id === selected);
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
- // Show current status
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
- * Block a feature
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 featureBlock(id, options) {
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
- const feature = await updateFeature(featureId, { status: 'blocked' });
251
- console.log(`\n\ud83d\uded1 Feature blocked: ${feature.name} (${feature.id})`);
252
- console.log(` Reason: ${options.reason}`);
253
- }
254
- /**
255
- * Helper to get item by index (1-based)
256
- */
257
- function getItemByIndex(items, index) {
258
- const itemIndex = index - 1; // Convert to 0-based
259
- if (itemIndex < 0 || itemIndex >= items.length) {
260
- throw new Error(`Invalid item index: ${index}. Feature has ${items.length} items.`);
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
- return items[itemIndex];
393
+ await writeFile(outputPath, markdown, 'utf-8');
394
+ console.log(`\n\u2705 Report generated: ${outputPath}`);
263
395
  }
264
396
  /**
265
- * Manage checklist items (interactive or with flags)
397
+ * Conclude the current session (even with incomplete items)
266
398
  */
267
- export async function featureManage(options = {}) {
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 use <id> to set an active 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
- // Check if any non-interactive flag is provided
276
- const hasNonInteractiveFlag = options.add !== undefined ||
277
- options.edit !== undefined ||
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
- // 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('');
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
- else {
353
- console.log('No checklist items yet.\n');
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 (action === 'add') {
372
- const { description } = await inquirer.prompt([
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}`);
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
- if (action === 'update') {
387
- if (feature.checklistItems.length === 0) {
388
- console.log('\nNo items to update.');
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}`);
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
- * Generate feature report
462
+ * Add a checklist item to the active feature
473
463
  */
474
- export async function featureReport(id, options) {
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
- console.log('\nGenerating report...');
481
- const reportData = await getFeatureReport(featureId);
482
- const markdown = generateFeatureReport(reportData);
483
- // Determine output path
484
- const outputPath = options.output ||
485
- join(process.cwd(), '.ranger', 'reports', `${featureId}.md`);
486
- // Ensure directory exists
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
- await writeFile(outputPath, markdown, 'utf-8');
492
- console.log(`\n\u2705 Report generated: ${outputPath}`);
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