@ranger-testing/ranger-cli 1.1.6 → 2.0.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 (109) hide show
  1. package/README.md +47 -45
  2. package/build/cli.js +671 -291
  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 +5 -10
  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 +9 -15
  11. package/build/commands/config.js.map +1 -1
  12. package/build/commands/env.js +10 -13
  13. package/build/commands/env.js.map +1 -1
  14. package/build/commands/feature.js +138 -67
  15. package/build/commands/feature.js.map +1 -1
  16. package/build/commands/hook.js +9 -4
  17. package/build/commands/hook.js.map +1 -1
  18. package/build/commands/hooks/autoPrompt.js +32 -0
  19. package/build/commands/hooks/autoPrompt.js.map +1 -0
  20. package/build/commands/hooks/disable.js +8 -5
  21. package/build/commands/hooks/disable.js.map +1 -1
  22. package/build/commands/hooks/enable.js +16 -9
  23. package/build/commands/hooks/enable.js.map +1 -1
  24. package/build/commands/hooks/exitPlanMode.js +10 -10
  25. package/build/commands/hooks/exitPlanMode.js.map +1 -1
  26. package/build/commands/hooks/index.js +1 -0
  27. package/build/commands/hooks/index.js.map +1 -1
  28. package/build/commands/hooks/output.js +20 -2
  29. package/build/commands/hooks/output.js.map +1 -1
  30. package/build/commands/hooks/planReminder.js +9 -9
  31. package/build/commands/hooks/planReminder.js.map +1 -1
  32. package/build/commands/hooks/planStart.js +6 -6
  33. package/build/commands/hooks/planStart.js.map +1 -1
  34. package/build/commands/hooks/postEdit.js +6 -6
  35. package/build/commands/hooks/postEdit.js.map +1 -1
  36. package/build/commands/hooks/preCompact.js +5 -5
  37. package/build/commands/hooks/preCompact.js.map +1 -1
  38. package/build/commands/hooks/sessionEnd.js +8 -4
  39. package/build/commands/hooks/sessionEnd.js.map +1 -1
  40. package/build/commands/hooks/sessionStart.js +41 -25
  41. package/build/commands/hooks/sessionStart.js.map +1 -1
  42. package/build/commands/hooks/stopHook.js +30 -6
  43. package/build/commands/hooks/stopHook.js.map +1 -1
  44. package/build/commands/index.js +1 -2
  45. package/build/commands/index.js.map +1 -1
  46. package/build/commands/login.js +2 -5
  47. package/build/commands/login.js.map +1 -1
  48. package/build/commands/setupCi.js +189 -0
  49. package/build/commands/setupCi.js.map +1 -0
  50. package/build/commands/skillup.js +16 -68
  51. package/build/commands/skillup.js.map +1 -1
  52. package/build/commands/start.js +1 -1
  53. package/build/commands/start.js.map +1 -1
  54. package/build/commands/status.js +14 -13
  55. package/build/commands/status.js.map +1 -1
  56. package/build/commands/update.js +34 -5
  57. package/build/commands/update.js.map +1 -1
  58. package/build/commands/updateEnv.js +1 -1
  59. package/build/commands/updateEnv.js.map +1 -1
  60. package/build/commands/useEnv.js +1 -1
  61. package/build/commands/useEnv.js.map +1 -1
  62. package/build/commands/utils/activeProfile.js +76 -0
  63. package/build/commands/utils/activeProfile.js.map +1 -0
  64. package/build/commands/utils/browserSessionsApi.js +1 -1
  65. package/build/commands/utils/browserSessionsApi.js.map +1 -1
  66. package/build/commands/utils/desirePathLog.js +39 -34
  67. package/build/commands/utils/desirePathLog.js.map +1 -1
  68. package/build/commands/utils/deviceAuth.js +53 -5
  69. package/build/commands/utils/deviceAuth.js.map +1 -1
  70. package/build/commands/utils/environment.js +11 -12
  71. package/build/commands/utils/environment.js.map +1 -1
  72. package/build/commands/utils/featureApi.js +49 -46
  73. package/build/commands/utils/featureApi.js.map +1 -1
  74. package/build/commands/utils/featureReportGenerator.js +6 -6
  75. package/build/commands/utils/featureReportGenerator.js.map +1 -1
  76. package/build/commands/utils/keychain.js +1 -1
  77. package/build/commands/utils/localAgentInstallationsApi.js +1 -1
  78. package/build/commands/utils/profileMessages.js +8 -0
  79. package/build/commands/utils/profileMessages.js.map +1 -0
  80. package/build/commands/utils/profileSetupBanner.js +167 -0
  81. package/build/commands/utils/profileSetupBanner.js.map +1 -0
  82. package/build/commands/utils/retry.js +25 -0
  83. package/build/commands/utils/retry.js.map +1 -0
  84. package/build/commands/utils/sessionCache.js +17 -0
  85. package/build/commands/utils/sessionCache.js.map +1 -1
  86. package/build/commands/utils/settings.js +23 -2
  87. package/build/commands/utils/settings.js.map +1 -1
  88. package/build/commands/utils/skills.js +1 -1
  89. package/build/commands/utils/telemetry.js +254 -0
  90. package/build/commands/utils/telemetry.js.map +1 -0
  91. package/build/commands/utils/userApi.js +4 -4
  92. package/build/commands/utils/userApi.js.map +1 -1
  93. package/build/commands/verifyFeature.js +678 -407
  94. package/build/commands/verifyFeature.js.map +1 -1
  95. package/build/commands/verifyInBrowser.js +1 -1
  96. package/build/commands/verifyInBrowser.js.map +1 -1
  97. package/build/skills/ranger/SKILL.md +65 -64
  98. package/build/skills/ranger/create.md +31 -31
  99. package/build/skills/ranger/feedback.md +25 -17
  100. package/build/skills/ranger/start.md +37 -37
  101. package/build/skills/ranger/verify.md +59 -55
  102. package/package.json +1 -1
  103. package/scripts/postinstall.js +1 -1
  104. package/build/commands/dataMcpServer.js +0 -1
  105. package/build/commands/dataMcpServer.js.map +0 -1
  106. package/build/commands/utils/cliSecret.js +0 -1
  107. package/build/commands/utils/cliSecret.js.map +0 -1
  108. package/build/skills/bug-bash.md +0 -329
  109. package/build/skills/e2e-test-recommender.md +0 -168
package/build/cli.js CHANGED
@@ -18,53 +18,282 @@ function findProjectRoot() {
18
18
  // Load .env from project root (not just cwd)
19
19
  dotenv.config({ path: join(findProjectRoot(), '.env') });
20
20
  import yargs from 'yargs/yargs';
21
- import { addEnv, clean, login, start, useEnv, updateEnv, update, skillup, envList, hook, } from './commands/index.js';
21
+ import { addEnv, clean, login, start, setupCi, useEnv, updateEnv, update, skillup, envList, hook, } from './commands/index.js';
22
22
  import { authEncrypt } from './commands/authEncrypt.js';
23
23
  import { status } from './commands/status.js';
24
24
  import { configSet, configGet, configList, configUnset, } from './commands/config.js';
25
- import { dataMcpServer } from './commands/dataMcpServer.js';
26
- import { verifyInBrowser } from './commands/verifyInBrowser.js';
27
25
  import { verifyFeature } from './commands/verifyFeature.js';
28
- import { featureCreate, featureList, featureShow, featureResume, featureSessions, featureConcludeSession, featureAddChecklistItem, featureGetFeedback, featureDelete, featureRestore, } from './commands/feature.js';
29
- import { generateMarkdownReport, collectScreenshots, } from './commands/utils/reportGenerator.js';
26
+ import { featureCreate, featureList, featureShow, featureResume, featureAddScenario, featureEditScenario, featureGetReview, featureDelete, featureRestore, } from './commands/feature.js';
30
27
  import { logDesirePath, getErrorType, sanitizeArgs, } from './commands/utils/desirePathLog.js';
31
28
  import { getCurrentVersion } from './commands/utils/version.js';
29
+ import { withTelemetry, getCurrentCollector, } from './commands/utils/telemetry.js';
30
+ // Capture unhandled rejections for telemetry
31
+ process.on('unhandledRejection', async (reason) => {
32
+ const collector = getCurrentCollector();
33
+ if (collector) {
34
+ await collector.trackCommandError(reason);
35
+ }
36
+ process.exitCode = 1;
37
+ });
38
+ const rawArgs = process.argv.slice(2);
39
+ const TOP_LEVEL_HELP = `Usage: ranger <command> [options]
40
+
41
+ Commands:
42
+ setup [token] Initialize Ranger in your project
43
+ login Re-authenticate without full setup
44
+ skillup Install Ranger skills for Claude Code
45
+ clean Remove Ranger artifacts from the project
46
+ status Show version, org, skills, and profile status
47
+ update Update Ranger CLI to the latest version
48
+
49
+ profile <command> Manage profiles (add/use/ls/update/config/encrypt-auth)
50
+ add <profile-name> Add profile (options: --ci, --skip-auth)
51
+ use <profile-name> Switch active profile
52
+ ls List profiles
53
+ update <profile-name> Re-capture auth for a profile
54
+ encrypt-auth <profile> Encrypt auth.json for safe git storage
55
+ config <command> Set/get/list/unset profile config
56
+
57
+ create <name> Create a feature review with scenarios
58
+ list List feature reviews
59
+ show [id] Show feature review details
60
+ resume [id] Resume a feature review
61
+ add-scenario <description> Add a scenario to the active feature review
62
+ edit-scenario <description> Edit a scenario description
63
+ get-review [id] Show reviewer feedback
64
+ delete [id] Soft delete a feature review
65
+ restore <id> Restore a soft-deleted feature review
66
+
67
+ go Verify a scenario in the browser
68
+
69
+ Run \`ranger <command> --help\` for details.`;
70
+ const PROFILE_HELP = `Usage: ranger profile <command> [options]
71
+
72
+ Commands:
73
+ add <profile-name> Add profile (options: --ci, --skip-auth)
74
+ use <profile-name> Switch active profile
75
+ ls List profiles
76
+ update <profile-name> Re-capture auth for a profile
77
+ encrypt-auth <profile> Encrypt auth.json for safe git storage
78
+ config <command> Profile config (set/get/list/unset)
79
+
80
+ Examples:
81
+ ranger profile add local
82
+ ranger profile encrypt-auth ci
83
+ ranger profile config set local headless true`;
84
+ const PROFILE_CONFIG_HELP = `Usage: ranger profile config <command>
85
+
86
+ Commands:
87
+ set <profile> <key> <value> Set a config value
88
+ get <profile> <key> Get a config value
89
+ list <profile> List all config for a profile
90
+ unset <profile> <key> Remove a config value
91
+
92
+ Keys:
93
+ userAgent Browser user agent string
94
+ headless Run browser in headless mode (true/false)
95
+ storageState Path to auth state file (e.g., ./auth.json)
96
+ header.<name> Custom HTTP header (e.g., header.X-Test-Mode)
97
+
98
+ Examples:
99
+ ranger profile config set ci userAgent "Mozilla/5.0 (CI Bot)"
100
+ ranger profile config set ci headless true
101
+ ranger profile config set ci header.Authorization '\${AUTH_TOKEN}'`;
102
+ function warnRenamed(oldUsage, newUsage) {
103
+ console.error(`\n${oldUsage} is now ${newUsage}\n`);
104
+ }
105
+ function argUsed(flag) {
106
+ return rawArgs.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
107
+ }
108
+ function warnFlagRenamed(commandPrefix, oldFlag, newFlag) {
109
+ if (argUsed(oldFlag)) {
110
+ warnRenamed(`${commandPrefix} ${oldFlag}`, `${commandPrefix} ${newFlag}`);
111
+ }
112
+ }
113
+ async function runGoCommand(argv, isLegacy = false) {
114
+ if (isLegacy) {
115
+ warnRenamed('ranger verify-feature', 'ranger go');
116
+ }
117
+ warnFlagRenamed('ranger go', '--env', '--profile');
118
+ warnFlagRenamed('ranger go', '--task', '--notes');
119
+ warnFlagRenamed('ranger go', '--item', '--scenario');
120
+ const result = await verifyFeature({
121
+ profile: argv.profile ??
122
+ argv.env,
123
+ notes: argv.notes ??
124
+ argv.task,
125
+ scenario: argv.scenario ??
126
+ argv.item,
127
+ startPath: argv['start-path'],
128
+ debugOutcome: argv['debug-outcome'],
129
+ });
130
+ console.log('\n' + '='.repeat(60));
131
+ console.log(result.evaluation === 'verified'
132
+ ? ' VERIFIED'
133
+ : result.evaluation === 'incomplete'
134
+ ? ' INCOMPLETE'
135
+ : result.evaluation === 'partial'
136
+ ? ' PARTIAL'
137
+ : result.evaluation === 'blocked'
138
+ ? ' BLOCKED'
139
+ : ' FAILED');
140
+ console.log('='.repeat(60));
141
+ console.log(`Summary: ${result.summary}`);
142
+ console.log(`Evaluation: ${result.evaluation}`);
143
+ console.log(`Reason: ${result.evaluationReason}`);
144
+ if (result.issues?.length) {
145
+ console.log('\nIssues:');
146
+ result.issues.forEach((issue, i) => {
147
+ console.log(`\n${i + 1}. [${issue.severity}] ${issue.description}`);
148
+ });
149
+ }
150
+ process.exit(result.evaluation === 'verified' ? 0 : 1);
151
+ }
32
152
  // Setup yargs CLI
33
153
  yargs(process.argv.slice(2))
154
+ .scriptName('ranger')
155
+ .usage(TOP_LEVEL_HELP)
34
156
  .version(getCurrentVersion())
35
- .command('add env <env-name>', 'Add environment configuration', (yargs) => {
157
+ .command('create <name>', 'Create a new feature review with scenarios', (yargs) => {
36
158
  return yargs
37
- .positional('env-name', {
159
+ .positional('name', {
38
160
  type: 'string',
39
- description: 'Name of the environment (e.g., local, staging, prod)',
161
+ description: 'Feature review name',
40
162
  demandOption: true,
41
163
  })
42
- .option('ci', {
164
+ .option('description', {
165
+ type: 'string',
166
+ alias: 'd',
167
+ description: 'Feature review description',
168
+ })
169
+ .option('scenario', {
170
+ type: 'array',
171
+ alias: 'c',
172
+ description: 'Scenarios (use multiple -c flags for multiple scenarios)',
173
+ })
174
+ .option('checklist', {
175
+ type: 'array',
176
+ hidden: true,
177
+ });
178
+ }, async (argv) => {
179
+ warnFlagRenamed('ranger create', '--checklist', '--scenario');
180
+ const scenarios = argv.scenario ||
181
+ argv.checklist;
182
+ await withTelemetry('create', () => featureCreate(argv.name, {
183
+ description: argv.description,
184
+ scenarios,
185
+ }));
186
+ })
187
+ .command('list', 'List all feature reviews', (yargs) => {
188
+ return yargs
189
+ .option('current-branch', {
43
190
  type: 'boolean',
44
- description: 'Create CI environment (encrypted auth, committed to git)',
45
- default: false,
191
+ description: 'Filter to feature reviews for current git branch',
192
+ })
193
+ .option('limit', {
194
+ type: 'number',
195
+ alias: 'l',
196
+ description: 'Maximum number of feature reviews to return',
197
+ default: 10,
46
198
  })
47
- .option('no-auth', {
199
+ .option('offset', {
200
+ type: 'number',
201
+ alias: 'o',
202
+ description: 'Number of feature reviews to skip',
203
+ default: 0,
204
+ })
205
+ .option('include-deleted', {
48
206
  type: 'boolean',
49
- description: 'Skip browser authentication (just save URL and settings)',
207
+ alias: 'd',
208
+ description: 'Include soft-deleted feature reviews',
50
209
  default: false,
51
210
  });
52
211
  }, async (argv) => {
53
- await addEnv(argv['env-name'], {
54
- ci: argv.ci,
55
- noAuth: argv['no-auth'],
212
+ await withTelemetry('list', () => featureList({
213
+ currentBranch: argv['current-branch'],
214
+ limit: argv.limit,
215
+ offset: argv.offset,
216
+ includeDeleted: argv['include-deleted'],
217
+ }));
218
+ })
219
+ .command('show [id]', 'Show feature review details (uses active feature review if no id)', (yargs) => {
220
+ return yargs.positional('id', {
221
+ type: 'string',
222
+ description: 'Feature review ID',
56
223
  });
224
+ }, async (argv) => {
225
+ await withTelemetry('show', () => featureShow(argv.id));
57
226
  })
58
- .command('use <env-name>', 'Switch to using a specific environment', (yargs) => {
59
- return yargs.positional('env-name', {
227
+ .command('resume [id]', 'Find and use feature review matching current git context', (yargs) => {
228
+ return yargs.positional('id', {
60
229
  type: 'string',
61
- description: 'Name of the environment',
230
+ description: 'Feature review ID (optional - bypasses search/prompt)',
231
+ });
232
+ }, async (argv) => {
233
+ await withTelemetry('resume', () => featureResume(argv.id));
234
+ })
235
+ .command('add-scenario <description>', 'Add a scenario to the active feature review', (yargs) => {
236
+ return yargs
237
+ .positional('description', {
238
+ type: 'string',
239
+ description: 'Scenario description',
62
240
  demandOption: true,
241
+ })
242
+ .option('id', {
243
+ type: 'string',
244
+ description: 'Feature review ID (uses active feature review if not provided)',
63
245
  });
64
246
  }, async (argv) => {
65
- await useEnv(argv['env-name']);
247
+ await withTelemetry('add-scenario', () => featureAddScenario(argv.description, argv.id));
66
248
  })
67
- .command('start [token]', 'Initialize Ranger in your project', (yargs) => {
249
+ .command('edit-scenario <description>', 'Edit a scenario description on the active feature review', (yargs) => {
250
+ return yargs
251
+ .positional('description', {
252
+ type: 'string',
253
+ description: 'New scenario description',
254
+ demandOption: true,
255
+ })
256
+ .option('scenario', {
257
+ type: 'number',
258
+ description: 'Scenario number to edit (1-based)',
259
+ demandOption: true,
260
+ })
261
+ .option('id', {
262
+ type: 'string',
263
+ description: 'Feature review ID (uses active feature review if not provided)',
264
+ });
265
+ }, async (argv) => {
266
+ await withTelemetry('edit-scenario', () => featureEditScenario(argv.description, {
267
+ id: argv.id,
268
+ scenario: argv.scenario,
269
+ }));
270
+ })
271
+ .command('get-review [id]', 'Show reviewer feedback (comments) for all scenarios', (yargs) => {
272
+ return yargs.positional('id', {
273
+ type: 'string',
274
+ description: 'Feature review ID (uses active feature review if not provided)',
275
+ });
276
+ }, async (argv) => {
277
+ await withTelemetry('get-review', () => featureGetReview(argv.id));
278
+ })
279
+ .command('delete [id]', 'Soft delete a feature review (uses active feature review if no id)', (yargs) => {
280
+ return yargs.positional('id', {
281
+ type: 'string',
282
+ description: 'Feature review ID',
283
+ });
284
+ }, async (argv) => {
285
+ await withTelemetry('delete', () => featureDelete(argv.id));
286
+ })
287
+ .command('restore <id>', 'Restore a soft-deleted feature review', (yargs) => {
288
+ return yargs.positional('id', {
289
+ type: 'string',
290
+ description: 'Feature review ID to restore',
291
+ demandOption: true,
292
+ });
293
+ }, async (argv) => {
294
+ await withTelemetry('restore', () => featureRestore(argv.id));
295
+ })
296
+ .command('setup [token]', 'Initialize Ranger in your project', (yargs) => {
68
297
  return yargs
69
298
  .positional('token', {
70
299
  type: 'string',
@@ -76,22 +305,331 @@ yargs(process.argv.slice(2))
76
305
  default: false,
77
306
  });
78
307
  }, async (argv) => {
79
- await start(argv.token, {
308
+ await withTelemetry('start', (telemetry) => start(argv.token, {
80
309
  skipChromium: argv['skip-chromium'],
310
+ }, telemetry));
311
+ })
312
+ .command('setup-ci <token>', 'Set up Ranger for CI (non-interactive)', (yargs) => {
313
+ return yargs
314
+ .positional('token', {
315
+ type: 'string',
316
+ description: 'Ranger API token',
317
+ demandOption: true,
318
+ })
319
+ .option('profile', {
320
+ type: 'string',
321
+ description: 'CI profile name (auto-detected if only one exists)',
322
+ })
323
+ .option('base-url', {
324
+ type: 'string',
325
+ description: 'Base URL for the app (creates/updates profile settings)',
326
+ })
327
+ .option('skip-chromium', {
328
+ type: 'boolean',
329
+ description: 'Skip Chromium browser installation',
330
+ default: false,
81
331
  });
332
+ }, async (argv) => {
333
+ await withTelemetry('setup-ci', (telemetry) => setupCi(argv.token, {
334
+ profile: argv.profile,
335
+ baseUrl: argv['base-url'],
336
+ skipChromium: argv['skip-chromium'],
337
+ }, telemetry));
82
338
  })
83
339
  .command('login', 'Log in to Ranger via browser (re-authenticate without full setup)', () => { }, async () => {
84
- await login();
340
+ await withTelemetry('login', () => login());
85
341
  })
86
342
  .command('skillup', 'Install Ranger skills for Claude Code', () => { }, async () => {
87
- await skillup();
343
+ await withTelemetry('skillup', () => skillup());
344
+ })
345
+ .command('clean', 'Remove all Ranger artifacts from the project', () => { }, async () => {
346
+ await withTelemetry('clean', () => clean());
347
+ })
348
+ .command('profile', 'Manage profiles', (yargs) => {
349
+ return yargs
350
+ .usage(PROFILE_HELP)
351
+ .command('add <profile-name>', 'Add profile configuration', (yargs) => {
352
+ return yargs
353
+ .positional('profile-name', {
354
+ type: 'string',
355
+ description: 'Name of the profile (e.g., local, staging, prod)',
356
+ demandOption: true,
357
+ })
358
+ .option('ci', {
359
+ type: 'boolean',
360
+ description: 'Create CI profile (encrypted auth, committed to git)',
361
+ default: false,
362
+ })
363
+ .option('skip-auth', {
364
+ type: 'boolean',
365
+ description: 'Skip browser authentication (just save URL and settings)',
366
+ default: false,
367
+ });
368
+ }, async (argv) => {
369
+ await withTelemetry('profile add', (telemetry) => addEnv(argv['profile-name'], {
370
+ ci: argv.ci,
371
+ skipAuth: argv['skip-auth'],
372
+ }, telemetry));
373
+ })
374
+ .command('use <profile-name>', 'Switch to using a specific profile', (yargs) => {
375
+ return yargs.positional('profile-name', {
376
+ type: 'string',
377
+ description: 'Name of the profile',
378
+ demandOption: true,
379
+ });
380
+ }, async (argv) => {
381
+ await withTelemetry('profile use', () => useEnv(argv['profile-name']));
382
+ })
383
+ .command('encrypt-auth <profile>', 'Encrypt auth.json for a profile (allows committing to git)', (yargs) => {
384
+ return yargs.positional('profile', {
385
+ type: 'string',
386
+ description: 'Profile name',
387
+ demandOption: true,
388
+ });
389
+ }, async (argv) => {
390
+ await withTelemetry('profile encrypt-auth', () => authEncrypt(argv.profile));
391
+ })
392
+ .command('ls', 'List all profiles', () => { }, async () => {
393
+ await withTelemetry('profile ls', () => envList());
394
+ })
395
+ .command('update <profile-name>', 'Update authentication for an existing profile', (yargs) => {
396
+ return yargs.positional('profile-name', {
397
+ type: 'string',
398
+ description: 'Name of the profile to update',
399
+ demandOption: true,
400
+ });
401
+ }, async (argv) => {
402
+ await withTelemetry('profile update', (telemetry) => updateEnv(argv['profile-name'], telemetry));
403
+ })
404
+ .command('config', 'Manage profile configuration', (yargs) => {
405
+ return yargs
406
+ .usage(PROFILE_CONFIG_HELP)
407
+ .command('set <profile> <key> <value>', 'Set a config value', (yargs) => {
408
+ return yargs
409
+ .positional('profile', {
410
+ type: 'string',
411
+ description: 'Profile name',
412
+ demandOption: true,
413
+ })
414
+ .positional('key', {
415
+ type: 'string',
416
+ description: 'Config key (e.g., userAgent, header.X-Custom)',
417
+ demandOption: true,
418
+ })
419
+ .positional('value', {
420
+ type: 'string',
421
+ description: 'Config value',
422
+ demandOption: true,
423
+ });
424
+ }, async (argv) => {
425
+ await withTelemetry('profile config set', () => configSet(argv.profile, argv.key, argv.value));
426
+ })
427
+ .command('get <profile> <key>', 'Get a config value', (yargs) => {
428
+ return yargs
429
+ .positional('profile', {
430
+ type: 'string',
431
+ description: 'Profile name',
432
+ demandOption: true,
433
+ })
434
+ .positional('key', {
435
+ type: 'string',
436
+ description: 'Config key',
437
+ demandOption: true,
438
+ });
439
+ }, async (argv) => {
440
+ await withTelemetry('profile config get', () => configGet(argv.profile, argv.key));
441
+ })
442
+ .command('list <profile>', 'List all config for a profile', (yargs) => {
443
+ return yargs.positional('profile', {
444
+ type: 'string',
445
+ description: 'Profile name',
446
+ demandOption: true,
447
+ });
448
+ }, async (argv) => {
449
+ await withTelemetry('profile config list', () => configList(argv.profile));
450
+ })
451
+ .command('unset <profile> <key>', 'Remove a config value', (yargs) => {
452
+ return yargs
453
+ .positional('profile', {
454
+ type: 'string',
455
+ description: 'Profile name',
456
+ demandOption: true,
457
+ })
458
+ .positional('key', {
459
+ type: 'string',
460
+ description: 'Config key to remove',
461
+ demandOption: true,
462
+ });
463
+ }, async (argv) => {
464
+ await withTelemetry('profile config unset', () => configUnset(argv.profile, argv.key));
465
+ })
466
+ .demandCommand(1, 'You must specify a profile config subcommand');
467
+ })
468
+ .demandCommand(1, 'You must specify a profile subcommand');
469
+ })
470
+ .command('status', 'Show Ranger status (version, skills, profiles)', () => { }, async () => {
471
+ await withTelemetry('status', () => status());
472
+ })
473
+ .command('update', 'Update Ranger CLI to the latest version', () => { }, async () => {
474
+ await withTelemetry('update', () => update());
475
+ })
476
+ .command('go', 'Verify a scenario in the browser (requires active feature review)', (yargs) => {
477
+ return yargs
478
+ .option('profile', {
479
+ type: 'string',
480
+ description: 'Profile to use (defaults to active profile)',
481
+ })
482
+ .option('notes', {
483
+ type: 'string',
484
+ description: 'Notes for verification (defaults to scenario description)',
485
+ })
486
+ .option('scenario', {
487
+ type: 'number',
488
+ description: 'Scenario index (1-based)',
489
+ })
490
+ .option('start-path', {
491
+ type: 'string',
492
+ description: 'Path to start on (appended to base URL, e.g., /dashboard)',
493
+ })
494
+ .option('debug-outcome', {
495
+ type: 'string',
496
+ hidden: true,
497
+ choices: [
498
+ 'verified',
499
+ 'partial',
500
+ 'blocked',
501
+ 'failed',
502
+ 'incomplete',
503
+ ],
504
+ })
505
+ .option('env', {
506
+ type: 'string',
507
+ hidden: true,
508
+ })
509
+ .option('task', {
510
+ type: 'string',
511
+ hidden: true,
512
+ })
513
+ .option('item', {
514
+ type: 'number',
515
+ hidden: true,
516
+ });
517
+ }, async (argv) => {
518
+ await runGoCommand(argv);
519
+ })
520
+ // Legacy command shims (hidden from help)
521
+ .command('auth', false, (yargs) => {
522
+ return yargs
523
+ .command('encrypt <profile>', false, (yargs) => {
524
+ return yargs.positional('profile', {
525
+ type: 'string',
526
+ description: 'Profile name',
527
+ demandOption: true,
528
+ });
529
+ }, async (argv) => {
530
+ warnRenamed('ranger auth encrypt', 'ranger profile encrypt-auth');
531
+ await authEncrypt(argv.profile);
532
+ })
533
+ .demandCommand(1, 'You must specify an auth subcommand');
534
+ }, () => { })
535
+ .command('start [token]', false, (yargs) => {
536
+ return yargs
537
+ .positional('token', {
538
+ type: 'string',
539
+ description: 'Ranger API token (omit to log in via browser)',
540
+ })
541
+ .option('skip-chromium', {
542
+ type: 'boolean',
543
+ description: 'Skip Chromium browser check and installation',
544
+ default: false,
545
+ });
546
+ }, async (argv) => {
547
+ warnRenamed('ranger start', 'ranger setup');
548
+ await withTelemetry('start', (telemetry) => start(argv.token, {
549
+ skipChromium: argv['skip-chromium'],
550
+ }, telemetry));
551
+ })
552
+ .command('verify-feature', false, (yargs) => {
553
+ return yargs
554
+ .option('env', {
555
+ type: 'string',
556
+ description: 'Profile to use (defaults to active profile)',
557
+ })
558
+ .option('task', {
559
+ type: 'string',
560
+ description: 'Task description (defaults to scenario description)',
561
+ })
562
+ .option('item', {
563
+ type: 'number',
564
+ description: 'Scenario index (1-based)',
565
+ })
566
+ .option('profile', {
567
+ type: 'string',
568
+ hidden: true,
569
+ })
570
+ .option('notes', {
571
+ type: 'string',
572
+ hidden: true,
573
+ })
574
+ .option('scenario', {
575
+ type: 'number',
576
+ hidden: true,
577
+ })
578
+ .option('start-path', {
579
+ type: 'string',
580
+ description: 'Path to start on (appended to base URL, e.g., /dashboard)',
581
+ })
582
+ .option('debug-outcome', {
583
+ type: 'string',
584
+ hidden: true,
585
+ choices: [
586
+ 'verified',
587
+ 'partial',
588
+ 'blocked',
589
+ 'failed',
590
+ 'incomplete',
591
+ ],
592
+ });
593
+ }, async (argv) => {
594
+ await runGoCommand(argv, true);
595
+ })
596
+ .command('add env <env-name>', false, (yargs) => {
597
+ return yargs
598
+ .positional('env-name', {
599
+ type: 'string',
600
+ description: 'Name of the environment (e.g., local, staging, prod)',
601
+ demandOption: true,
602
+ })
603
+ .option('ci', {
604
+ type: 'boolean',
605
+ description: 'Create CI environment (encrypted auth, committed to git)',
606
+ default: false,
607
+ })
608
+ .option('skip-auth', {
609
+ type: 'boolean',
610
+ description: 'Skip browser authentication (just save URL and settings)',
611
+ default: false,
612
+ });
613
+ }, async (argv) => {
614
+ warnRenamed('ranger add env', 'ranger profile add');
615
+ await withTelemetry('add env', (telemetry) => addEnv(argv['env-name'], {
616
+ ci: argv.ci,
617
+ skipAuth: argv['skip-auth'],
618
+ }, telemetry));
88
619
  })
89
- .command('clean', 'Remove all Ranger artifacts from the project', async () => {
90
- await clean();
620
+ .command('use <env-name>', false, (yargs) => {
621
+ return yargs.positional('env-name', {
622
+ type: 'string',
623
+ description: 'Name of the environment',
624
+ demandOption: true,
625
+ });
626
+ }, async (argv) => {
627
+ warnRenamed('ranger use', 'ranger profile use');
628
+ await withTelemetry('use', () => useEnv(argv['env-name']));
91
629
  })
92
- .command('config', 'Manage environment configuration', (yargs) => {
630
+ .command('config', false, (yargs) => {
93
631
  return yargs
94
- .command('set <env> <key> <value>', 'Set a config value', (yargs) => {
632
+ .command('set <env> <key> <value>', false, (yargs) => {
95
633
  return yargs
96
634
  .positional('env', {
97
635
  type: 'string',
@@ -109,9 +647,10 @@ yargs(process.argv.slice(2))
109
647
  demandOption: true,
110
648
  });
111
649
  }, async (argv) => {
112
- await configSet(argv.env, argv.key, argv.value);
650
+ warnRenamed('ranger config set', 'ranger profile config set');
651
+ await withTelemetry('config set', () => configSet(argv.env, argv.key, argv.value));
113
652
  })
114
- .command('get <env> <key>', 'Get a config value', (yargs) => {
653
+ .command('get <env> <key>', false, (yargs) => {
115
654
  return yargs
116
655
  .positional('env', {
117
656
  type: 'string',
@@ -124,18 +663,20 @@ yargs(process.argv.slice(2))
124
663
  demandOption: true,
125
664
  });
126
665
  }, async (argv) => {
127
- await configGet(argv.env, argv.key);
666
+ warnRenamed('ranger config get', 'ranger profile config get');
667
+ await withTelemetry('config get', () => configGet(argv.env, argv.key));
128
668
  })
129
- .command('list <env>', 'List all config for an environment', (yargs) => {
669
+ .command('list <env>', false, (yargs) => {
130
670
  return yargs.positional('env', {
131
671
  type: 'string',
132
672
  description: 'Environment name',
133
673
  demandOption: true,
134
674
  });
135
675
  }, async (argv) => {
136
- await configList(argv.env);
676
+ warnRenamed('ranger config list', 'ranger profile config list');
677
+ await withTelemetry('config list', () => configList(argv.env));
137
678
  })
138
- .command('unset <env> <key>', 'Remove a config value', (yargs) => {
679
+ .command('unset <env> <key>', false, (yargs) => {
139
680
  return yargs
140
681
  .positional('env', {
141
682
  type: 'string',
@@ -148,368 +689,195 @@ yargs(process.argv.slice(2))
148
689
  demandOption: true,
149
690
  });
150
691
  }, async (argv) => {
151
- await configUnset(argv.env, argv.key);
692
+ warnRenamed('ranger config unset', 'ranger profile config unset');
693
+ await withTelemetry('config unset', () => configUnset(argv.env, argv.key));
152
694
  })
153
- .demandCommand(1, 'You must specify a config subcommand')
154
- .epilogue(`Supported keys:
155
- userAgent Browser user agent string
156
- headless Run browser in headless mode (true/false)
157
- storageState Path to auth state file (e.g., ./auth.json)
158
- header.<name> Custom HTTP header (e.g., header.X-Test-Mode)
159
-
160
- Examples:
161
- ranger config set ci userAgent "Mozilla/5.0 (CI Bot)"
162
- ranger config set ci headless true
163
- ranger config set ci header.Authorization '\${AUTH_TOKEN}'`);
164
- })
165
- .command('auth', 'Manage authentication', (yargs) => {
166
- return yargs
167
- .command('encrypt <env>', 'Encrypt auth.json for an environment (allows committing to git)', (yargs) => {
168
- return yargs.positional('env', {
169
- type: 'string',
170
- description: 'Environment name',
171
- demandOption: true,
172
- });
173
- }, async (argv) => {
174
- await authEncrypt(argv.env);
175
- })
176
- .demandCommand(1, 'You must specify an auth subcommand')
177
- .epilogue(`Commands:
178
- encrypt <env> Encrypt auth.json for safe git storage
179
-
180
- Examples:
181
- ranger auth encrypt local # Encrypts .ranger/local/auth.json -> auth.json.enc
182
- ranger auth encrypt ci # Encrypts .ranger/ci/auth.json -> auth.json.enc`);
695
+ .demandCommand(1, 'You must specify a config subcommand');
183
696
  })
184
- .command('env', 'Manage environments', (yargs) => {
697
+ .command('env', false, (yargs) => {
185
698
  return yargs
186
- .command('ls', 'List all environments', () => { }, async () => {
187
- await envList();
699
+ .command('ls', false, () => { }, async () => {
700
+ warnRenamed('ranger env ls', 'ranger profile ls');
701
+ await withTelemetry('env list', () => envList());
188
702
  })
189
- .command('update <env-name>', 'Update authentication for an existing environment', (yargs) => {
703
+ .command('update <env-name>', false, (yargs) => {
190
704
  return yargs.positional('env-name', {
191
705
  type: 'string',
192
706
  description: 'Name of the environment to update',
193
707
  demandOption: true,
194
708
  });
195
709
  }, async (argv) => {
196
- await updateEnv(argv['env-name']);
710
+ warnRenamed('ranger env update', 'ranger profile update');
711
+ await withTelemetry('env update', (telemetry) => updateEnv(argv['env-name'], telemetry));
197
712
  })
198
- .demandCommand(1, 'You must specify an env subcommand, try ranger env help')
199
- .epilogue(`Commands:
200
- ls List all environments
201
- update <env> Update authentication for an environment
202
-
203
- Examples:
204
- ranger env ls # List all environments
205
- ranger env update local # Update auth for local environment`);
206
- })
207
- .command('data-mcp-server', 'Run MCP proxy server (reads local credentials)', async () => {
208
- await dataMcpServer();
713
+ .demandCommand(1, 'You must specify an env subcommand');
209
714
  })
210
- .command('verify-in-browser', false, (yargs) => {
715
+ .command('feature', false, (yargs) => {
211
716
  return yargs
212
- .option('task', {
213
- type: 'string',
214
- description: 'Description of UI flow to verify',
215
- demandOption: true,
216
- })
217
- .option('env', {
218
- type: 'string',
219
- description: 'Environment to use (defaults to active environment)',
220
- })
221
- .option('headed', {
222
- type: 'boolean',
223
- description: 'Run browser in headed mode (default: headless)',
224
- default: false,
225
- });
226
- }, async (argv) => {
227
- const result = await verifyInBrowser(argv.task, {
228
- env: argv.env,
229
- headed: argv.headed,
230
- });
231
- console.log('\n' + '='.repeat(60));
232
- console.log(result.success ? ' VERIFIED' : ' ISSUES FOUND');
233
- console.log('='.repeat(60));
234
- console.log(result.summary);
235
- if (result.issues?.length) {
236
- console.log('\nIssues:');
237
- result.issues.forEach((issue, i) => {
238
- console.log(`\n${i + 1}. [${issue.severity}] ${issue.description}`);
239
- if (issue.screenshot)
240
- console.log(` Screenshot: ${issue.screenshot}`);
241
- });
242
- }
243
- if (result.traceViewerUrl) {
244
- console.log(`\nFull Trace: ${result.traceViewerUrl}`);
245
- }
246
- // Generate and output markdown report with delimiters
247
- if (result.sessionId && result.sessionDir) {
248
- const { screenshots } = await collectScreenshots(result.sessionDir);
249
- const markdownReport = generateMarkdownReport({
250
- sessionId: result.sessionId,
251
- task: result.task || argv.task,
252
- url: result.url || '',
253
- success: result.success,
254
- summary: result.summary,
255
- issues: result.issues?.filter((issue) => issue.severity !== 'MINOR'),
256
- screenshots,
257
- durationMs: result.durationMs || 0,
258
- traceViewerUrl: result.traceViewerUrl,
259
- });
260
- console.log('\n' + '='.repeat(60));
261
- console.log('VERIFICATION_REPORT_START');
262
- console.log('='.repeat(60));
263
- console.log(markdownReport);
264
- console.log('='.repeat(60));
265
- console.log('VERIFICATION_REPORT_END');
266
- console.log('='.repeat(60));
267
- }
268
- process.exit(result.success ? 0 : 1);
269
- })
270
- .command('status', 'Show Ranger status (version, skills, environments)', () => { }, async () => {
271
- await status();
272
- })
273
- .command('update', 'Update Ranger CLI to the latest version', () => { }, async () => {
274
- await update();
275
- })
276
- .command('feature', 'Manage feature tracking', (yargs) => {
277
- return yargs
278
- .command('create <name>', 'Create a new feature with checklist', (yargs) => {
717
+ .command('create <name>', false, (yargs) => {
279
718
  return yargs
280
719
  .positional('name', {
281
720
  type: 'string',
282
- description: 'Feature name',
721
+ description: 'Feature review name',
283
722
  demandOption: true,
284
723
  })
285
724
  .option('description', {
286
725
  type: 'string',
287
726
  alias: 'd',
288
- description: 'Feature description',
727
+ description: 'Feature review description',
289
728
  })
290
729
  .option('checklist', {
291
730
  type: 'array',
292
731
  alias: 'c',
293
- description: 'Checklist items (use multiple -c flags for multiple items)',
732
+ description: 'Scenarios (use multiple -c flags for multiple scenarios)',
733
+ })
734
+ .option('scenario', {
735
+ type: 'array',
736
+ hidden: true,
294
737
  });
295
738
  }, async (argv) => {
296
- await featureCreate(argv.name, {
739
+ warnRenamed('ranger feature create', 'ranger create');
740
+ warnFlagRenamed('ranger create', '--checklist', '--scenario');
741
+ const scenarios = argv.scenario ||
742
+ argv.checklist;
743
+ await withTelemetry('feature create', () => featureCreate(argv.name, {
297
744
  description: argv.description,
298
- checklist: argv.checklist,
299
- });
745
+ scenarios,
746
+ }));
300
747
  })
301
- .command('list', 'List all features', (yargs) => {
748
+ .command('list', false, (yargs) => {
302
749
  return yargs
303
750
  .option('current-branch', {
304
751
  type: 'boolean',
305
- description: 'Filter to features for current git branch',
752
+ description: 'Filter to feature reviews for current git branch',
306
753
  })
307
754
  .option('limit', {
308
755
  type: 'number',
309
756
  alias: 'l',
310
- description: 'Maximum number of features to return',
757
+ description: 'Maximum number of feature reviews to return',
311
758
  default: 10,
312
759
  })
313
760
  .option('offset', {
314
761
  type: 'number',
315
762
  alias: 'o',
316
- description: 'Number of features to skip',
763
+ description: 'Number of feature reviews to skip',
317
764
  default: 0,
318
765
  })
319
766
  .option('include-deleted', {
320
767
  type: 'boolean',
321
768
  alias: 'd',
322
- description: 'Include soft-deleted features',
769
+ description: 'Include soft-deleted feature reviews',
323
770
  default: false,
324
771
  });
325
772
  }, async (argv) => {
326
- await featureList({
773
+ warnRenamed('ranger feature list', 'ranger list');
774
+ await withTelemetry('feature list', () => featureList({
327
775
  currentBranch: argv['current-branch'],
328
776
  limit: argv.limit,
329
777
  offset: argv.offset,
330
778
  includeDeleted: argv['include-deleted'],
331
- });
779
+ }));
332
780
  })
333
- .command('show [id]', 'Show feature details (uses active feature if no id)', (yargs) => {
781
+ .command('show [id]', false, (yargs) => {
334
782
  return yargs.positional('id', {
335
783
  type: 'string',
336
- description: 'Feature ID',
784
+ description: 'Feature review ID',
337
785
  });
338
786
  }, async (argv) => {
339
- await featureShow(argv.id);
787
+ warnRenamed('ranger feature show', 'ranger show');
788
+ await withTelemetry('feature show', () => featureShow(argv.id));
340
789
  })
341
- .command('resume [id]', 'Find and use feature matching current git context', (yargs) => {
790
+ .command('resume [id]', false, (yargs) => {
342
791
  return yargs.positional('id', {
343
792
  type: 'string',
344
- description: 'Feature ID (optional - bypasses search/prompt)',
793
+ description: 'Feature review ID (optional - bypasses search/prompt)',
345
794
  });
346
795
  }, async (argv) => {
347
- await featureResume(argv.id);
796
+ warnRenamed('ranger feature resume', 'ranger resume');
797
+ await withTelemetry('feature resume', () => featureResume(argv.id));
348
798
  })
349
- .command('sessions [id]', 'List sessions for a feature', (yargs) => {
350
- return yargs.positional('id', {
351
- type: 'string',
352
- description: 'Feature ID (uses active feature if not provided)',
353
- });
354
- }, async (argv) => {
355
- await featureSessions(argv.id);
356
- })
357
- .command('conclude-session [id]', 'End the current session (even with incomplete items)', (yargs) => {
358
- return yargs.positional('id', {
359
- type: 'string',
360
- description: 'Feature ID (uses active feature if not provided)',
361
- });
362
- }, async (argv) => {
363
- await featureConcludeSession(argv.id);
364
- })
365
- .command('add-checklist-item <description>', 'Add a checklist item to the active feature', (yargs) => {
799
+ .command('add-checklist-item <description>', false, (yargs) => {
366
800
  return yargs
367
801
  .positional('description', {
368
802
  type: 'string',
369
- description: 'Checklist item description',
803
+ description: 'Scenario description',
370
804
  demandOption: true,
371
805
  })
372
806
  .option('id', {
373
807
  type: 'string',
374
- description: 'Feature ID (uses active feature if not provided)',
808
+ description: 'Feature review ID (uses active feature review if not provided)',
375
809
  });
376
810
  }, async (argv) => {
377
- await featureAddChecklistItem(argv.description, argv.id);
811
+ warnRenamed('ranger feature add-checklist-item', 'ranger add-scenario');
812
+ await withTelemetry('feature add-checklist-item', () => featureAddScenario(argv.description, argv.id));
378
813
  })
379
- .command('get-feedback [id]', 'Show reviewer feedback (comments) for all action items', (yargs) => {
814
+ .command('get-feedback [id]', false, (yargs) => {
380
815
  return yargs.positional('id', {
381
816
  type: 'string',
382
- description: 'Feature ID (uses active feature if not provided)',
817
+ description: 'Feature review ID (uses active feature review if not provided)',
383
818
  });
384
819
  }, async (argv) => {
385
- await featureGetFeedback(argv.id);
820
+ warnRenamed('ranger feature get-feedback', 'ranger get-review');
821
+ await withTelemetry('feature get-feedback', () => featureGetReview(argv.id));
386
822
  })
387
- .command('delete [id]', 'Soft delete a feature (uses active feature if no id)', (yargs) => {
823
+ .command('delete [id]', false, (yargs) => {
388
824
  return yargs.positional('id', {
389
825
  type: 'string',
390
- description: 'Feature ID',
826
+ description: 'Feature review ID',
391
827
  });
392
828
  }, async (argv) => {
393
- await featureDelete(argv.id);
829
+ warnRenamed('ranger feature delete', 'ranger delete');
830
+ await withTelemetry('feature delete', () => featureDelete(argv.id));
394
831
  })
395
- .command('restore <id>', 'Restore a soft-deleted feature', (yargs) => {
832
+ .command('restore <id>', false, (yargs) => {
396
833
  return yargs.positional('id', {
397
834
  type: 'string',
398
- description: 'Feature ID to restore',
835
+ description: 'Feature review ID to restore',
399
836
  demandOption: true,
400
837
  });
401
838
  }, async (argv) => {
402
- await featureRestore(argv.id);
403
- })
404
- .demandCommand(1, 'You must specify a feature subcommand')
405
- .epilogue(`Commands:
406
- create <name> Create a new feature with checklist
407
- list List all features
408
- show [id] Show feature details
409
- resume [id] Resume a feature by ID
410
- sessions [id] List sessions for a feature
411
- conclude-session [id] End the current session (even with incomplete items)
412
- add-checklist-item Add a checklist item to the active feature
413
- get-feedback [id] Show reviewer feedback for action items
414
- delete [id] Soft delete a feature
415
- restore <id> Restore a soft-deleted feature
416
-
417
- Examples:
418
- ranger feature create "User auth" -c "Login works" -c "Signup works"
419
- ranger feature list --current-branch
420
- ranger feature list --include-deleted
421
- ranger feature show
422
- ranger feature resume feat_abc123
423
- ranger feature add-checklist-item "User can reset password"
424
- ranger feature get-feedback
425
- ranger feature delete
426
- ranger feature restore feat_abc123`);
427
- })
428
- .command('verify-feature', 'Verify a checklist item in the browser (requires active feature)', (yargs) => {
429
- return yargs
430
- .option('env', {
431
- type: 'string',
432
- description: 'Environment to use (defaults to active environment)',
433
- })
434
- .option('task', {
435
- type: 'string',
436
- description: 'Task description (defaults to checklist item description)',
839
+ warnRenamed('ranger feature restore', 'ranger restore');
840
+ await withTelemetry('feature restore', () => featureRestore(argv.id));
437
841
  })
438
- .option('item', {
439
- type: 'number',
440
- description: 'Checklist item index (1-based)',
441
- })
442
- .option('start-path', {
443
- type: 'string',
444
- description: 'Path to start on (appended to base URL, e.g., /dashboard)',
445
- })
446
- .option('debug-outcome', {
447
- type: 'string',
448
- hidden: true,
449
- choices: [
450
- 'verified',
451
- 'partial',
452
- 'blocked',
453
- 'failed',
454
- 'incomplete',
455
- ],
456
- });
457
- }, async (argv) => {
458
- const result = await verifyFeature({
459
- env: argv.env,
460
- task: argv.task,
461
- item: argv.item,
462
- startPath: argv['start-path'],
463
- debugOutcome: argv['debug-outcome'],
464
- });
465
- console.log('\n' + '='.repeat(60));
466
- console.log(result.evaluation === 'verified'
467
- ? ' VERIFIED'
468
- : result.evaluation === 'partial'
469
- ? ' PARTIAL'
470
- : result.evaluation === 'blocked'
471
- ? ' BLOCKED'
472
- : ' FAILED');
473
- console.log('='.repeat(60));
474
- console.log(`Summary: ${result.summary}`);
475
- console.log(`Evaluation: ${result.evaluation}`);
476
- console.log(`Reason: ${result.evaluationReason}`);
477
- if (result.issues?.length) {
478
- console.log('\nIssues:');
479
- result.issues.forEach((issue, i) => {
480
- console.log(`\n${i + 1}. [${issue.severity}] ${issue.description}`);
481
- });
482
- }
483
- process.exit(result.evaluation === 'verified' ? 0 : 1);
842
+ .demandCommand(1, 'You must specify a feature review subcommand');
484
843
  })
485
- // Unified hook command (hidden from help - invoked automatically by Claude Code plugin)
844
+ // Unified hook command (hidden from help - invoked automatically by Claude Code / OpenCode plugins)
486
845
  .command('hook', false, (yargs) => {
487
846
  return yargs
488
- .command('enable', 'Enable Ranger hooks for this session', () => { }, async () => {
489
- await hook('enable');
847
+ .command('enable', 'Enable Ranger hooks for this session', (yargs) => {
848
+ return yargs.option('session-id', {
849
+ type: 'string',
850
+ description: 'Session ID (for non-Claude integrations like OpenCode)',
851
+ });
852
+ }, async (argv) => {
853
+ await hook('enable', argv['session-id']);
490
854
  })
491
- .command('disable', 'Disable Ranger hooks for this session', () => { }, async () => {
492
- await hook('disable');
855
+ .command('disable', 'Disable Ranger hooks for this session', (yargs) => {
856
+ return yargs.option('session-id', {
857
+ type: 'string',
858
+ description: 'Session ID (for non-Claude integrations like OpenCode)',
859
+ });
860
+ }, async (argv) => {
861
+ await hook('disable', argv['session-id']);
493
862
  })
494
863
  .option('name', {
495
864
  type: 'string',
496
865
  description: 'Hook name (session-start, pre-compact, post-edit, etc.)',
866
+ })
867
+ .option('session-id', {
868
+ type: 'string',
869
+ description: 'Session ID (for non-Claude integrations like OpenCode)',
497
870
  });
498
871
  }, async (argv) => {
499
872
  if (argv.name) {
500
- await hook(argv.name);
873
+ await hook(argv.name, argv['session-id']);
501
874
  }
502
875
  })
503
876
  .demandCommand(1, 'You must specify a command')
504
877
  .strictCommands()
505
- .fail((msg, err, yargs) => {
878
+ .fail(async (msg, err, yargs) => {
506
879
  const rawCommand = sanitizeArgs(process.argv.slice(2));
507
880
  const errorType = getErrorType(msg, err);
508
- logDesirePath({
509
- rawCommand,
510
- errorMessage: msg || err?.message,
511
- errorType,
512
- });
513
881
  if (msg && msg.includes('Unknown command')) {
514
882
  const command = process.argv[2];
515
883
  console.error(`\nUnknown command: ${command}`);
@@ -521,9 +889,21 @@ Examples:
521
889
  else if (err) {
522
890
  console.error(`\nError: ${err.message}\n`);
523
891
  }
524
- // Small delay allows desire path log fetch to dispatch before exit
525
- setTimeout(() => process.exit(1), 50);
892
+ await logDesirePath({
893
+ rawCommand,
894
+ errorMessage: msg || err?.message,
895
+ errorType,
896
+ });
897
+ // Report to telemetry if there's an active collector
898
+ const collector = getCurrentCollector();
899
+ if (collector) {
900
+ await collector.trackCommandError(err || new Error(msg || 'Unknown error'));
901
+ }
902
+ process.exit(1);
526
903
  })
904
+ .epilogue('Documentation:\n' +
905
+ ' https://docs.ranger.net\n' +
906
+ ' https://docs.ranger.net/llms.txt (for LLMs)')
527
907
  .help()
528
908
  .alias('help', 'h')
529
909
  .parse();