@xera-ai/cli 0.8.1 → 0.9.1

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 (2) hide show
  1. package/dist/index.js +150 -79
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -266,10 +266,36 @@ var TEMPLATE_DIR = TEMPLATE_ROOT;
266
266
 
267
267
  // src/commands/init.ts
268
268
  var require2 = createRequire(import.meta.url);
269
+ function cancel2() {
270
+ p.cancel("Aborted.");
271
+ process.exit(0);
272
+ }
273
+ async function prompt(flag, defaultValue, ask) {
274
+ if (flag !== undefined)
275
+ return flag;
276
+ if (defaultValue !== undefined)
277
+ return defaultValue;
278
+ const val = await ask();
279
+ if (typeof val === "symbol")
280
+ cancel2();
281
+ return val;
282
+ }
269
283
  async function initCommand(opts) {
270
284
  const cwd = process.cwd();
285
+ if (!process.stdin.isTTY && !opts.yes) {
286
+ console.error(pc2.red(`
287
+ error: stdin is not a TTY \u2014 interactive prompts cannot run.
288
+ `));
289
+ console.error(` Pass ${pc2.cyan("-y / --yes")} and use flags to run non-interactively:
290
+ `);
291
+ console.error(` xera init -y --shape web --pk MYPROJ --ju https://myco.atlassian.net
292
+ `);
293
+ console.error(` Run ${pc2.cyan("xera init --help")} to see all available flags.
294
+ `);
295
+ process.exit(1);
296
+ }
271
297
  p.intro(pc2.cyan("xera \u2014 project setup"));
272
- const shape = opts.shape ?? (opts.yes ? "web" : await p.select({
298
+ const shape = await prompt(opts.shape, opts.yes ? "web" : undefined, () => p.select({
273
299
  message: "What kind of testing does this project do?",
274
300
  initialValue: "web",
275
301
  options: [
@@ -278,68 +304,37 @@ async function initCommand(opts) {
278
304
  { value: "mixed", label: "Both (some UI tickets, some API tickets, in one repo)" }
279
305
  ]
280
306
  }));
281
- if (typeof shape === "symbol") {
282
- p.cancel("Aborted.");
283
- process.exit(0);
284
- }
285
307
  const wantsWeb = shape === "web" || shape === "mixed";
286
308
  const wantsHttp = shape === "api" || shape === "mixed";
287
- const baseAnswers = opts.yes ? {
288
- jiraBaseUrl: "https://example.atlassian.net",
289
- projectKeys: "JIRA",
290
- storyField: "description",
291
- acceptanceCriteriaField: ""
292
- } : await p.group({
293
- jiraBaseUrl: () => p.text({ message: "Jira workspace URL", placeholder: "https://x.atlassian.net" }),
294
- projectKeys: () => p.text({
295
- message: "Jira project key(s) (comma-separated)",
296
- placeholder: "JIRA"
297
- }),
298
- storyField: () => p.text({ message: "Jira field id for user story", initialValue: "description" }),
299
- acceptanceCriteriaField: () => p.text({
300
- message: "Jira field id for AC (leave empty if same as story)",
301
- initialValue: ""
302
- })
303
- }, {
304
- onCancel: () => {
305
- p.cancel("Aborted.");
306
- process.exit(0);
307
- }
308
- });
309
- const webAnswers = !wantsWeb ? null : opts.yes ? { stagingUrl: "http://localhost:3000", authEnabled: true, roles: "admin,regular" } : await p.group({
310
- stagingUrl: () => p.text({
309
+ const jiraBaseUrl = await prompt(opts.jiraBaseUrl, opts.yes ? "https://example.atlassian.net" : undefined, () => p.text({ message: "Jira workspace URL", placeholder: "https://x.atlassian.net" }));
310
+ const projectKeys = await prompt(opts.projectKeys, opts.yes ? "JIRA" : undefined, () => p.text({ message: "Jira project key(s) (comma-separated)", placeholder: "JIRA" }));
311
+ const storyField = await prompt(opts.storyField, opts.yes ? "description" : undefined, () => p.text({ message: "Jira field id for user story", initialValue: "description" }));
312
+ const acceptanceCriteriaField = await prompt(opts.acField, opts.yes ? "" : undefined, () => p.text({
313
+ message: "Jira field id for AC (leave empty if same as story)",
314
+ initialValue: ""
315
+ }));
316
+ let stagingUrl = "";
317
+ let authEnabled = false;
318
+ let roles = "";
319
+ if (wantsWeb) {
320
+ stagingUrl = await prompt(opts.stagingUrl, opts.yes ? "http://localhost:3000" : undefined, () => p.text({
311
321
  message: "Web app staging URL",
312
322
  placeholder: "https://staging.example.com"
313
- }),
314
- authEnabled: () => p.confirm({
315
- message: "Does your app require login to test most pages?",
316
- initialValue: true
317
- }),
318
- roles: () => p.text({
319
- message: "Test user roles (comma-separated)",
320
- initialValue: "admin,regular"
321
- })
322
- }, {
323
- onCancel: () => {
324
- p.cancel("Aborted.");
325
- process.exit(0);
326
- }
327
- });
328
- const httpAnswers = !wantsHttp ? null : opts.yes ? {
329
- apiBaseUrl: "http://localhost:4111",
330
- openapiPath: "./openapi.yaml",
331
- authStrategy: "bearer",
332
- httpRoles: "user"
333
- } : await p.group({
334
- apiBaseUrl: () => p.text({
335
- message: "API base URL",
336
- placeholder: "https://api.staging.example.com"
337
- }),
338
- openapiPath: () => p.text({
323
+ }));
324
+ authEnabled = await prompt(opts.authEnabled, opts.yes ? true : undefined, () => p.confirm({ message: "Does your app require login to test most pages?", initialValue: true }));
325
+ roles = await prompt(opts.roles, opts.yes ? "admin,regular" : undefined, () => p.text({ message: "Test user roles (comma-separated)", initialValue: "admin,regular" }));
326
+ }
327
+ let apiBaseUrl = "";
328
+ let openapiPath = "";
329
+ let authStrategy = "none";
330
+ let httpRoles = "";
331
+ if (wantsHttp) {
332
+ apiBaseUrl = await prompt(opts.apiBaseUrl, opts.yes ? "http://localhost:4111" : undefined, () => p.text({ message: "API base URL", placeholder: "https://api.staging.example.com" }));
333
+ openapiPath = await prompt(opts.openapiPath, opts.yes ? "./openapi.yaml" : undefined, () => p.text({
339
334
  message: "OpenAPI spec path (relative or URL \u2014 leave empty to skip)",
340
335
  initialValue: "./openapi.yaml"
341
- }),
342
- authStrategy: () => p.select({
336
+ }));
337
+ authStrategy = await prompt(opts.authStrategy, opts.yes ? "bearer" : undefined, () => p.select({
343
338
  message: "API auth strategy",
344
339
  initialValue: "bearer",
345
340
  options: [
@@ -349,29 +344,24 @@ async function initCommand(opts) {
349
344
  { value: "oauth-cc", label: "OAuth client_credentials" },
350
345
  { value: "none", label: "None / public API" }
351
346
  ]
352
- }),
353
- httpRoles: () => p.text({ message: "HTTP roles (comma-separated)", initialValue: "user" })
354
- }, {
355
- onCancel: () => {
356
- p.cancel("Aborted.");
357
- process.exit(0);
358
- }
359
- });
347
+ }));
348
+ httpRoles = await prompt(opts.httpRoles, opts.yes ? "user" : undefined, () => p.text({ message: "HTTP roles (comma-separated)", initialValue: "user" }));
349
+ }
360
350
  const vars = {
361
351
  shape,
362
352
  wantsWeb,
363
353
  wantsHttp,
364
- jiraBaseUrl: baseAnswers.jiraBaseUrl,
365
- projectKeys: baseAnswers.projectKeys.split(",").map((s) => s.trim()).filter(Boolean),
366
- storyField: baseAnswers.storyField,
367
- acceptanceCriteriaField: baseAnswers.acceptanceCriteriaField,
368
- stagingUrl: webAnswers?.stagingUrl ?? "",
369
- authEnabled: !!webAnswers?.authEnabled,
370
- roles: webAnswers ? webAnswers.roles.split(",").map((s) => s.trim()).filter(Boolean) : [],
371
- apiBaseUrl: httpAnswers?.apiBaseUrl ?? "",
372
- openapiPath: httpAnswers?.openapiPath ?? "",
373
- authStrategy: httpAnswers?.authStrategy ?? "none",
374
- httpRoles: httpAnswers ? httpAnswers.httpRoles.split(",").map((s) => s.trim()).filter(Boolean) : [],
354
+ jiraBaseUrl,
355
+ projectKeys: projectKeys.split(",").map((s) => s.trim()).filter(Boolean),
356
+ storyField,
357
+ acceptanceCriteriaField,
358
+ stagingUrl,
359
+ authEnabled,
360
+ roles: roles ? roles.split(",").map((s) => s.trim()).filter(Boolean) : [],
361
+ apiBaseUrl,
362
+ openapiPath,
363
+ authStrategy,
364
+ httpRoles: httpRoles ? httpRoles.split(",").map((s) => s.trim()).filter(Boolean) : [],
375
365
  authKey: generateKey()
376
366
  };
377
367
  const configTmpl = shape === "web" ? "xera.config.ts.tmpl" : shape === "api" ? "http-xera.config.ts.tmpl" : "mixed-xera.config.ts.tmpl";
@@ -585,11 +575,50 @@ async function initUpdateCommand(_opts) {
585
575
  var require4 = createRequire3(import.meta.url);
586
576
  var VERSION = require4("../package.json").version;
587
577
  var VALID_SHAPES = ["web", "api", "mixed"];
578
+ var VALID_AUTH_STRATEGIES = ["bearer", "apiKey", "basic", "oauth-cc", "none"];
579
+ var KNOWN_COMMANDS = ["init", "doctor"];
580
+ function levenshtein(a, b) {
581
+ const m = a.length;
582
+ const n = b.length;
583
+ const dp = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0));
584
+ for (let i = 1;i <= m; i++) {
585
+ for (let j = 1;j <= n; j++) {
586
+ dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
587
+ }
588
+ }
589
+ return dp[m][n];
590
+ }
591
+ function didYouMean(input) {
592
+ let best;
593
+ let bestDist = Number.POSITIVE_INFINITY;
594
+ for (const cmd of KNOWN_COMMANDS) {
595
+ const d = levenshtein(input.toLowerCase(), cmd);
596
+ if (d < bestDist) {
597
+ bestDist = d;
598
+ best = cmd;
599
+ }
600
+ }
601
+ return bestDist <= 3 ? best : undefined;
602
+ }
603
+ function unknownCommand(input) {
604
+ console.error(pc4.red(`
605
+ error: Unknown command '${input}'
606
+ `));
607
+ const suggestion = didYouMean(input);
608
+ if (suggestion) {
609
+ console.error(` Did you mean ${pc4.cyan(suggestion)}?
610
+ `);
611
+ }
612
+ console.error(` Run ${pc4.cyan("xera --help")} to see available commands.
613
+ `);
614
+ process.exit(1);
615
+ }
588
616
  async function main() {
589
617
  const cli = cac("xera");
590
618
  cli.help();
591
619
  cli.version(VERSION);
592
- cli.command("init", "Scaffold a new xera project in the current directory").option("--update", "Non-destructive refresh of an existing project").option("-y, --yes", "Accept all defaults (non-interactive)").option("--shape <shape>", "Project shape: web | api | mixed").action(async (opts) => {
620
+ cli.usage("<command> [options]");
621
+ cli.command("init", "Scaffold a new xera project in the current directory").option("--update", "Non-destructive refresh of an existing project").option("-y, --yes", "Accept all defaults (non-interactive)").option("--shape <shape>", "Project shape: web | api | mixed").option("--ju, --jira-base-url <url>", "Jira workspace URL").option("--pk, --project-keys <keys>", "Jira project key(s), comma-separated").option("--sf, --story-field <field>", "Jira field id for user story (default: description)").option("--ac, --ac-field <field>", "Jira field id for acceptance criteria").option("--su, --staging-url <url>", "Web app staging URL").option("--auth-enabled", "App requires login to test most pages").option("--no-auth-enabled", "App does not require login").option("--ro, --roles <roles>", "Test user roles, comma-separated (default: admin,regular)").option("--au, --api-base-url <url>", "API base URL").option("--op, --openapi-path <path>", "OpenAPI spec path or URL").option("--as, --auth-strategy <strategy>", `API auth strategy: ${VALID_AUTH_STRATEGIES.join(" | ")}`).option("--hr, --http-roles <roles>", "HTTP test roles, comma-separated (default: user)").example("xera init").example("xera init -y --shape web").example("xera init -y --shape api --pk MYPROJ --ju https://myco.atlassian.net --au https://api.staging.example.com --as bearer").example("xera init -y --shape mixed --pk PROJ --ju https://myco.atlassian.net --su https://staging.example.com --au https://api.staging.example.com").action(async (opts) => {
593
622
  if (opts.update) {
594
623
  await initUpdateCommand({ yes: !!opts.yes });
595
624
  return;
@@ -597,22 +626,64 @@ async function main() {
597
626
  const initOpts = { yes: !!opts.yes };
598
627
  if (opts.shape !== undefined) {
599
628
  if (!VALID_SHAPES.includes(opts.shape)) {
600
- console.error(pc4.red(`[xera] --shape must be one of: ${VALID_SHAPES.join(", ")}`));
629
+ console.error(pc4.red(`
630
+ error: --shape must be one of: ${VALID_SHAPES.join(", ")}
631
+ `));
601
632
  process.exit(1);
602
633
  }
603
634
  initOpts.shape = opts.shape;
604
635
  }
636
+ if (opts.authStrategy !== undefined) {
637
+ if (!VALID_AUTH_STRATEGIES.includes(opts.authStrategy)) {
638
+ console.error(pc4.red(`
639
+ error: --auth-strategy must be one of: ${VALID_AUTH_STRATEGIES.join(", ")}
640
+ `));
641
+ process.exit(1);
642
+ }
643
+ initOpts.authStrategy = opts.authStrategy;
644
+ }
645
+ if (opts.jiraBaseUrl !== undefined)
646
+ initOpts.jiraBaseUrl = opts.jiraBaseUrl;
647
+ if (opts.projectKeys !== undefined)
648
+ initOpts.projectKeys = opts.projectKeys;
649
+ if (opts.storyField !== undefined)
650
+ initOpts.storyField = opts.storyField;
651
+ if (opts.acField !== undefined)
652
+ initOpts.acField = opts.acField;
653
+ if (opts.stagingUrl !== undefined)
654
+ initOpts.stagingUrl = opts.stagingUrl;
655
+ if (opts.authEnabled !== undefined)
656
+ initOpts.authEnabled = opts.authEnabled;
657
+ if (opts.roles !== undefined)
658
+ initOpts.roles = opts.roles;
659
+ if (opts.apiBaseUrl !== undefined)
660
+ initOpts.apiBaseUrl = opts.apiBaseUrl;
661
+ if (opts.openapiPath !== undefined)
662
+ initOpts.openapiPath = opts.openapiPath;
663
+ if (opts.httpRoles !== undefined)
664
+ initOpts.httpRoles = opts.httpRoles;
605
665
  await initCommand(initOpts);
606
666
  });
607
667
  cli.command("doctor", "Run a health check").option("--strict <ticket>", "Treat ticket-specific checks as required").option("--logs <ticket>", "Pretty-print xera.log for a ticket").option("--usage", "Show token/usage summary from recent runs").action(async (opts) => {
608
668
  const exit = await doctorCommand(opts);
609
669
  process.exit(exit);
610
670
  });
671
+ const rawArgs = process.argv.slice(2);
672
+ if (rawArgs.length === 0) {
673
+ cli.outputHelp();
674
+ process.exit(0);
675
+ }
676
+ const firstArg = rawArgs[0];
677
+ if (!firstArg.startsWith("-") && !KNOWN_COMMANDS.includes(firstArg)) {
678
+ unknownCommand(firstArg);
679
+ }
611
680
  try {
612
681
  cli.parse(process.argv, { run: false });
613
682
  await cli.runMatchedCommand();
614
683
  } catch (e) {
615
- console.error(pc4.red(`[xera] ${e.message}`));
684
+ console.error(pc4.red(`
685
+ error: ${e.message}
686
+ `));
616
687
  process.exit(1);
617
688
  }
618
689
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xera-ai/cli",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "xera": "./bin/xera"
@@ -15,8 +15,8 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "@xera-ai/core": "^0.8.1",
19
- "@xera-ai/skills": "^0.8.1",
18
+ "@xera-ai/core": "^0.9.1",
19
+ "@xera-ai/skills": "^0.9.1",
20
20
  "@clack/prompts": "1.4.0",
21
21
  "cac": "7.0.0",
22
22
  "picocolors": "1.1.1"