@ranger-testing/ranger-cli 1.1.7 → 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 (90) hide show
  1. package/README.md +47 -45
  2. package/build/cli.js +644 -277
  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/hooks/autoPrompt.js +1 -1
  17. package/build/commands/hooks/disable.js +1 -1
  18. package/build/commands/hooks/enable.js +9 -4
  19. package/build/commands/hooks/enable.js.map +1 -1
  20. package/build/commands/hooks/exitPlanMode.js +8 -8
  21. package/build/commands/hooks/planReminder.js +7 -7
  22. package/build/commands/hooks/planStart.js +4 -4
  23. package/build/commands/hooks/postEdit.js +4 -4
  24. package/build/commands/hooks/postEdit.js.map +1 -1
  25. package/build/commands/hooks/preCompact.js +3 -3
  26. package/build/commands/hooks/preCompact.js.map +1 -1
  27. package/build/commands/hooks/sessionStart.js +19 -5
  28. package/build/commands/hooks/sessionStart.js.map +1 -1
  29. package/build/commands/hooks/stopHook.js +28 -4
  30. package/build/commands/hooks/stopHook.js.map +1 -1
  31. package/build/commands/index.js +1 -2
  32. package/build/commands/index.js.map +1 -1
  33. package/build/commands/login.js +2 -5
  34. package/build/commands/login.js.map +1 -1
  35. package/build/commands/setupCi.js +189 -0
  36. package/build/commands/setupCi.js.map +1 -0
  37. package/build/commands/skillup.js +16 -68
  38. package/build/commands/skillup.js.map +1 -1
  39. package/build/commands/start.js +1 -1
  40. package/build/commands/start.js.map +1 -1
  41. package/build/commands/status.js +14 -13
  42. package/build/commands/status.js.map +1 -1
  43. package/build/commands/update.js +34 -5
  44. package/build/commands/update.js.map +1 -1
  45. package/build/commands/updateEnv.js +1 -1
  46. package/build/commands/updateEnv.js.map +1 -1
  47. package/build/commands/useEnv.js +1 -1
  48. package/build/commands/useEnv.js.map +1 -1
  49. package/build/commands/utils/activeProfile.js +76 -0
  50. package/build/commands/utils/activeProfile.js.map +1 -0
  51. package/build/commands/utils/browserSessionsApi.js +1 -1
  52. package/build/commands/utils/browserSessionsApi.js.map +1 -1
  53. package/build/commands/utils/deviceAuth.js +53 -5
  54. package/build/commands/utils/deviceAuth.js.map +1 -1
  55. package/build/commands/utils/environment.js +11 -12
  56. package/build/commands/utils/environment.js.map +1 -1
  57. package/build/commands/utils/featureApi.js +30 -30
  58. package/build/commands/utils/featureApi.js.map +1 -1
  59. package/build/commands/utils/featureReportGenerator.js +6 -6
  60. package/build/commands/utils/featureReportGenerator.js.map +1 -1
  61. package/build/commands/utils/keychain.js +1 -1
  62. package/build/commands/utils/localAgentInstallationsApi.js +1 -1
  63. package/build/commands/utils/profileMessages.js +8 -0
  64. package/build/commands/utils/profileMessages.js.map +1 -0
  65. package/build/commands/utils/profileSetupBanner.js +167 -0
  66. package/build/commands/utils/profileSetupBanner.js.map +1 -0
  67. package/build/commands/utils/settings.js +20 -2
  68. package/build/commands/utils/settings.js.map +1 -1
  69. package/build/commands/utils/skills.js +1 -1
  70. package/build/commands/utils/telemetry.js +254 -0
  71. package/build/commands/utils/telemetry.js.map +1 -0
  72. package/build/commands/utils/userApi.js +4 -4
  73. package/build/commands/utils/userApi.js.map +1 -1
  74. package/build/commands/verifyFeature.js +771 -526
  75. package/build/commands/verifyFeature.js.map +1 -1
  76. package/build/commands/verifyInBrowser.js +1 -1
  77. package/build/commands/verifyInBrowser.js.map +1 -1
  78. package/build/skills/ranger/SKILL.md +65 -64
  79. package/build/skills/ranger/create.md +31 -31
  80. package/build/skills/ranger/feedback.md +25 -17
  81. package/build/skills/ranger/start.md +37 -37
  82. package/build/skills/ranger/verify.md +59 -55
  83. package/package.json +1 -1
  84. package/scripts/postinstall.js +1 -1
  85. package/build/commands/dataMcpServer.js +0 -1
  86. package/build/commands/dataMcpServer.js.map +0 -1
  87. package/build/commands/utils/cliSecret.js +0 -1
  88. package/build/commands/utils/cliSecret.js.map +0 -1
  89. package/build/skills/bug-bash.md +0 -329
  90. 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',
46
192
  })
47
- .option('skip-auth', {
193
+ .option('limit', {
194
+ type: 'number',
195
+ alias: 'l',
196
+ description: 'Maximum number of feature reviews to return',
197
+ default: 10,
198
+ })
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
- skipAuth: argv['skip-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);
88
519
  })
89
- .command('clean', 'Remove all Ranger artifacts from the project', async () => {
90
- await clean();
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));
91
551
  })
92
- .command('config', 'Manage environment configuration', (yargs) => {
552
+ .command('verify-feature', false, (yargs) => {
93
553
  return yargs
94
- .command('set <env> <key> <value>', 'Set a config value', (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));
619
+ })
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']));
629
+ })
630
+ .command('config', false, (yargs) => {
631
+ return 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,339 +689,157 @@ 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}'`);
695
+ .demandCommand(1, 'You must specify a config subcommand');
164
696
  })
165
- .command('auth', 'Manage authentication', (yargs) => {
697
+ .command('env', false, (yargs) => {
166
698
  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);
699
+ .command('ls', false, () => { }, async () => {
700
+ warnRenamed('ranger env ls', 'ranger profile ls');
701
+ await withTelemetry('env list', () => envList());
175
702
  })
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`);
183
- })
184
- .command('env', 'Manage environments', (yargs) => {
185
- return yargs
186
- .command('ls', 'List all environments', () => { }, async () => {
187
- await envList();
188
- })
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
- });
332
- })
333
- .command('show [id]', 'Show feature details (uses active feature if no id)', (yargs) => {
334
- return yargs.positional('id', {
335
- type: 'string',
336
- description: 'Feature ID',
337
- });
338
- }, async (argv) => {
339
- await featureShow(argv.id);
340
- })
341
- .command('resume [id]', 'Find and use feature matching current git context', (yargs) => {
342
- return yargs.positional('id', {
343
- type: 'string',
344
- description: 'Feature ID (optional - bypasses search/prompt)',
345
- });
346
- }, async (argv) => {
347
- await featureResume(argv.id);
779
+ }));
348
780
  })
349
- .command('sessions [id]', 'List sessions for a feature', (yargs) => {
781
+ .command('show [id]', false, (yargs) => {
350
782
  return yargs.positional('id', {
351
783
  type: 'string',
352
- description: 'Feature ID (uses active feature if not provided)',
784
+ description: 'Feature review ID',
353
785
  });
354
786
  }, async (argv) => {
355
- await featureSessions(argv.id);
787
+ warnRenamed('ranger feature show', 'ranger show');
788
+ await withTelemetry('feature show', () => featureShow(argv.id));
356
789
  })
357
- .command('conclude-session [id]', 'End the current session (even with incomplete items)', (yargs) => {
790
+ .command('resume [id]', false, (yargs) => {
358
791
  return yargs.positional('id', {
359
792
  type: 'string',
360
- description: 'Feature ID (uses active feature if not provided)',
793
+ description: 'Feature review ID (optional - bypasses search/prompt)',
361
794
  });
362
795
  }, async (argv) => {
363
- await featureConcludeSession(argv.id);
796
+ warnRenamed('ranger feature resume', 'ranger resume');
797
+ await withTelemetry('feature resume', () => featureResume(argv.id));
364
798
  })
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)',
437
- })
438
- .option('item', {
439
- type: 'number',
440
- description: 'Checklist item index (1-based)',
839
+ warnRenamed('ranger feature restore', 'ranger restore');
840
+ await withTelemetry('feature restore', () => featureRestore(argv.id));
441
841
  })
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
844
  // Unified hook command (hidden from help - invoked automatically by Claude Code / OpenCode plugins)
486
845
  .command('hook', false, (yargs) => {
@@ -535,8 +894,16 @@ Examples:
535
894
  errorMessage: msg || err?.message,
536
895
  errorType,
537
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
+ }
538
902
  process.exit(1);
539
903
  })
904
+ .epilogue('Documentation:\n' +
905
+ ' https://docs.ranger.net\n' +
906
+ ' https://docs.ranger.net/llms.txt (for LLMs)')
540
907
  .help()
541
908
  .alias('help', 'h')
542
909
  .parse();