@xera-ai/cli 0.8.1 → 0.9.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 (2) hide show
  1. package/dist/index.js +84 -77
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -266,10 +266,24 @@ 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();
271
285
  p.intro(pc2.cyan("xera \u2014 project setup"));
272
- const shape = opts.shape ?? (opts.yes ? "web" : await p.select({
286
+ const shape = await prompt(opts.shape, opts.yes ? "web" : undefined, () => p.select({
273
287
  message: "What kind of testing does this project do?",
274
288
  initialValue: "web",
275
289
  options: [
@@ -278,68 +292,37 @@ async function initCommand(opts) {
278
292
  { value: "mixed", label: "Both (some UI tickets, some API tickets, in one repo)" }
279
293
  ]
280
294
  }));
281
- if (typeof shape === "symbol") {
282
- p.cancel("Aborted.");
283
- process.exit(0);
284
- }
285
295
  const wantsWeb = shape === "web" || shape === "mixed";
286
296
  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({
297
+ const jiraBaseUrl = await prompt(opts.jiraBaseUrl, opts.yes ? "https://example.atlassian.net" : undefined, () => p.text({ message: "Jira workspace URL", placeholder: "https://x.atlassian.net" }));
298
+ const projectKeys = await prompt(opts.projectKeys, opts.yes ? "JIRA" : undefined, () => p.text({ message: "Jira project key(s) (comma-separated)", placeholder: "JIRA" }));
299
+ const storyField = await prompt(opts.storyField, opts.yes ? "description" : undefined, () => p.text({ message: "Jira field id for user story", initialValue: "description" }));
300
+ const acceptanceCriteriaField = await prompt(opts.acField, opts.yes ? "" : undefined, () => p.text({
301
+ message: "Jira field id for AC (leave empty if same as story)",
302
+ initialValue: ""
303
+ }));
304
+ let stagingUrl = "";
305
+ let authEnabled = false;
306
+ let roles = "";
307
+ if (wantsWeb) {
308
+ stagingUrl = await prompt(opts.stagingUrl, opts.yes ? "http://localhost:3000" : undefined, () => p.text({
311
309
  message: "Web app staging URL",
312
310
  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({
311
+ }));
312
+ authEnabled = await prompt(opts.authEnabled, opts.yes ? true : undefined, () => p.confirm({ message: "Does your app require login to test most pages?", initialValue: true }));
313
+ roles = await prompt(opts.roles, opts.yes ? "admin,regular" : undefined, () => p.text({ message: "Test user roles (comma-separated)", initialValue: "admin,regular" }));
314
+ }
315
+ let apiBaseUrl = "";
316
+ let openapiPath = "";
317
+ let authStrategy = "none";
318
+ let httpRoles = "";
319
+ if (wantsHttp) {
320
+ apiBaseUrl = await prompt(opts.apiBaseUrl, opts.yes ? "http://localhost:4111" : undefined, () => p.text({ message: "API base URL", placeholder: "https://api.staging.example.com" }));
321
+ openapiPath = await prompt(opts.openapiPath, opts.yes ? "./openapi.yaml" : undefined, () => p.text({
339
322
  message: "OpenAPI spec path (relative or URL \u2014 leave empty to skip)",
340
323
  initialValue: "./openapi.yaml"
341
- }),
342
- authStrategy: () => p.select({
324
+ }));
325
+ authStrategy = await prompt(opts.authStrategy, opts.yes ? "bearer" : undefined, () => p.select({
343
326
  message: "API auth strategy",
344
327
  initialValue: "bearer",
345
328
  options: [
@@ -349,29 +332,24 @@ async function initCommand(opts) {
349
332
  { value: "oauth-cc", label: "OAuth client_credentials" },
350
333
  { value: "none", label: "None / public API" }
351
334
  ]
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
- });
335
+ }));
336
+ httpRoles = await prompt(opts.httpRoles, opts.yes ? "user" : undefined, () => p.text({ message: "HTTP roles (comma-separated)", initialValue: "user" }));
337
+ }
360
338
  const vars = {
361
339
  shape,
362
340
  wantsWeb,
363
341
  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) : [],
342
+ jiraBaseUrl,
343
+ projectKeys: projectKeys.split(",").map((s) => s.trim()).filter(Boolean),
344
+ storyField,
345
+ acceptanceCriteriaField,
346
+ stagingUrl,
347
+ authEnabled,
348
+ roles: roles ? roles.split(",").map((s) => s.trim()).filter(Boolean) : [],
349
+ apiBaseUrl,
350
+ openapiPath,
351
+ authStrategy,
352
+ httpRoles: httpRoles ? httpRoles.split(",").map((s) => s.trim()).filter(Boolean) : [],
375
353
  authKey: generateKey()
376
354
  };
377
355
  const configTmpl = shape === "web" ? "xera.config.ts.tmpl" : shape === "api" ? "http-xera.config.ts.tmpl" : "mixed-xera.config.ts.tmpl";
@@ -585,11 +563,13 @@ async function initUpdateCommand(_opts) {
585
563
  var require4 = createRequire3(import.meta.url);
586
564
  var VERSION = require4("../package.json").version;
587
565
  var VALID_SHAPES = ["web", "api", "mixed"];
566
+ var VALID_AUTH_STRATEGIES = ["bearer", "apiKey", "basic", "oauth-cc", "none"];
588
567
  async function main() {
589
568
  const cli = cac("xera");
590
569
  cli.help();
591
570
  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) => {
571
+ cli.usage("<command> [options]");
572
+ 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
573
  if (opts.update) {
594
574
  await initUpdateCommand({ yes: !!opts.yes });
595
575
  return;
@@ -602,6 +582,33 @@ async function main() {
602
582
  }
603
583
  initOpts.shape = opts.shape;
604
584
  }
585
+ if (opts.authStrategy !== undefined) {
586
+ if (!VALID_AUTH_STRATEGIES.includes(opts.authStrategy)) {
587
+ console.error(pc4.red(`[xera] --auth-strategy must be one of: ${VALID_AUTH_STRATEGIES.join(", ")}`));
588
+ process.exit(1);
589
+ }
590
+ initOpts.authStrategy = opts.authStrategy;
591
+ }
592
+ if (opts.jiraBaseUrl !== undefined)
593
+ initOpts.jiraBaseUrl = opts.jiraBaseUrl;
594
+ if (opts.projectKeys !== undefined)
595
+ initOpts.projectKeys = opts.projectKeys;
596
+ if (opts.storyField !== undefined)
597
+ initOpts.storyField = opts.storyField;
598
+ if (opts.acField !== undefined)
599
+ initOpts.acField = opts.acField;
600
+ if (opts.stagingUrl !== undefined)
601
+ initOpts.stagingUrl = opts.stagingUrl;
602
+ if (opts.authEnabled !== undefined)
603
+ initOpts.authEnabled = opts.authEnabled;
604
+ if (opts.roles !== undefined)
605
+ initOpts.roles = opts.roles;
606
+ if (opts.apiBaseUrl !== undefined)
607
+ initOpts.apiBaseUrl = opts.apiBaseUrl;
608
+ if (opts.openapiPath !== undefined)
609
+ initOpts.openapiPath = opts.openapiPath;
610
+ if (opts.httpRoles !== undefined)
611
+ initOpts.httpRoles = opts.httpRoles;
605
612
  await initCommand(initOpts);
606
613
  });
607
614
  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) => {
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.0",
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.0",
19
+ "@xera-ai/skills": "^0.9.0",
20
20
  "@clack/prompts": "1.4.0",
21
21
  "cac": "7.0.0",
22
22
  "picocolors": "1.1.1"