@xera-ai/cli 0.11.6 → 0.12.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.
package/dist/index.js CHANGED
@@ -480,6 +480,8 @@ async function initCommand(opts) {
480
480
  pkg.scripts["xera:impact-prepare"] = "xera-internal impact-prepare";
481
481
  pkg.scripts["xera:heal-prepare"] = "xera-internal heal-prepare";
482
482
  pkg.scripts["xera:disputes"] = "xera-internal disputes";
483
+ pkg.scripts["xera:explore-prepare"] = "xera-internal explore-prepare";
484
+ pkg.scripts["xera:explore-finalize"] = "xera-internal explore-finalize";
483
485
  pkg.dependencies = pkg.dependencies ?? {};
484
486
  pkg.dependencies["@xera-ai/core"] = `^${CLI_VERSION}`;
485
487
  pkg.dependencies["@xera-ai/prompts"] = `^${CLI_VERSION}`;
@@ -494,22 +496,25 @@ async function initCommand(opts) {
494
496
  writeFileSync2(pkgPath, JSON.stringify(pkg, null, 2));
495
497
  const nextSteps = shape === "api" ? `
496
498
  Next:
497
- 1) Set your auth credentials in .env.local:
498
- USER_BEARER_TOKEN=...
499
+ 1) Copy .env.example to .env and set your auth credentials:
500
+ cp .env.example .env
501
+ # then edit .env to set USER_BEARER_TOKEN=...
499
502
  2) Run pre-authentication:
500
503
  bun run xera:auth-setup
501
504
  3) Start testing:
502
505
  Open Claude Code in this directory and run: /xera-run <TICKET>
503
506
  ` : shape === "mixed" ? `
504
507
  Next:
505
- 1) Set credentials in .env.local (both web logins and API tokens)
508
+ 1) Copy .env.example to .env and set credentials (both web logins and API tokens):
509
+ cp .env.example .env
506
510
  2) Run pre-authentication:
507
511
  bun run xera:auth-setup
508
512
  3) Start testing:
509
513
  Open Claude Code in this directory and run: /xera-run <TICKET>
510
514
  ` : `
511
515
  Next:
512
- 1) Set your Jira credentials in .env.local
516
+ 1) Copy .env.example to .env and set your Jira credentials:
517
+ cp .env.example .env
513
518
  2) Run pre-authentication:
514
519
  bun run xera:auth-setup
515
520
  3) Start testing:
@@ -534,7 +539,85 @@ import * as p2 from "@clack/prompts";
534
539
  import pc3 from "picocolors";
535
540
  var require3 = createRequire2(import.meta.url);
536
541
  var CLI_VERSION2 = require3("../package.json").version;
537
- async function initUpdateCommand(_opts) {
542
+ function detectAdaptersFromConfig(cwd) {
543
+ const configPath = join4(cwd, "xera.config.ts");
544
+ if (!existsSync4(configPath))
545
+ return null;
546
+ const cfg = readFileSync4(configPath, "utf8");
547
+ const m = cfg.match(/adapters:\s*\[([^\]]+)\]/);
548
+ if (!m)
549
+ return null;
550
+ return (m[1].match(/'(\w+)'/g) ?? []).map((s) => s.slice(1, -1));
551
+ }
552
+ function adaptersForShape(shape) {
553
+ if (shape === "web")
554
+ return ["web"];
555
+ if (shape === "api")
556
+ return ["http"];
557
+ return ["web", "http"];
558
+ }
559
+ function renderHttpConfigSnippet(opts) {
560
+ const baseUrl = opts.apiBaseUrl ?? "https://api.staging.example.com";
561
+ const strategy = opts.authStrategy ?? "bearer";
562
+ const roles = (opts.httpRoles ?? "user").split(",").map((s) => s.trim()).filter(Boolean);
563
+ const rolesBlock = roles.map((r) => ` ${r}: { tokenEnv: '${r.toUpperCase().replace(/-/g, "_")}_BEARER_TOKEN' },`).join(`
564
+ `);
565
+ const specLine = opts.openapiPath ? ` spec: '${opts.openapiPath}',
566
+ ` : "";
567
+ return [
568
+ ` http: {`,
569
+ ` baseUrl: { staging: '${baseUrl}' },`,
570
+ ` defaultEnv: 'staging',`,
571
+ `${specLine} auth: {`,
572
+ ` strategy: '${strategy}',`,
573
+ ` roles: {`,
574
+ rolesBlock,
575
+ ` },`,
576
+ ` },`,
577
+ ` },`
578
+ ].join(`
579
+ `);
580
+ }
581
+ function renderWebConfigSnippet(opts) {
582
+ const baseUrl = opts.stagingUrl ?? "https://staging.example.com";
583
+ const roles = (opts.roles ?? "admin,regular").split(",").map((s) => s.trim()).filter(Boolean);
584
+ const authBlock = opts.authEnabled === false ? "" : ` auth: {
585
+ strategy: 'storageState',
586
+ setupScript: './shared/auth-setup.ts',
587
+ roles: {
588
+ ${roles.map((r) => ` ${r}: { envEmail: 'TEST_${r.toUpperCase().replace(/-/g, "_")}_EMAIL', envPassword: 'TEST_${r.toUpperCase().replace(/-/g, "_")}_PWD' },`).join(`
589
+ `)}
590
+ },
591
+ },
592
+ `;
593
+ return [
594
+ ` web: {`,
595
+ ` baseUrl: { staging: '${baseUrl}' },`,
596
+ ` defaultEnv: 'staging',`,
597
+ authBlock + ` },`
598
+ ].join(`
599
+ `);
600
+ }
601
+ var HTTP_AUTH_SETUP_SNIPPET = `import { defineHttpAuthSetup, presetHttpAuth } from '@xera-ai/http';
602
+
603
+ export const http = defineHttpAuthSetup(async (request, role, creds) => {
604
+ return presetHttpAuth({
605
+ request,
606
+ role,
607
+ config: (globalThis as Record<string, unknown>).__XERA_HTTP_CONFIG__ as never,
608
+ });
609
+ });`;
610
+ var WEB_AUTH_SETUP_SNIPPET = `import { defineAuthSetup } from '@xera-ai/web';
611
+
612
+ export const web = defineAuthSetup(async (page, _role, creds) => {
613
+ await page.goto('/login');
614
+ await page.getByLabel('Email').fill(creds.email);
615
+ await page.getByLabel('Password').fill(creds.password);
616
+ await page.getByRole('button', { name: 'Sign in' }).click();
617
+ await page.waitForURL(/.*\\/dashboard/);
618
+ return { expiresAt: Date.now() + 8 * 3600 * 1000 };
619
+ });`;
620
+ async function initUpdateCommand(opts) {
538
621
  const cwd = process.cwd();
539
622
  p2.intro(pc3.cyan("xera init --update"));
540
623
  const pkgPath = join4(cwd, "package.json");
@@ -616,6 +699,51 @@ async function initUpdateCommand(_opts) {
616
699
  p2.log.warn(`kept local ${name}`);
617
700
  }
618
701
  }
702
+ const hasShapeFlags = opts.apiBaseUrl !== undefined || opts.openapiPath !== undefined || opts.authStrategy !== undefined || opts.httpRoles !== undefined || opts.stagingUrl !== undefined || opts.authEnabled !== undefined || opts.roles !== undefined;
703
+ if (opts.shape !== undefined) {
704
+ const current = detectAdaptersFromConfig(cwd);
705
+ if (current === null) {
706
+ p2.log.warn("--shape was provided but could not detect existing adapters in xera.config.ts. Skipping shape check.");
707
+ } else {
708
+ const requested = adaptersForShape(opts.shape);
709
+ const missing = requested.filter((a) => !current.includes(a));
710
+ const extra = current.filter((a) => !requested.includes(a));
711
+ if (missing.length === 0 && extra.length === 0) {
712
+ p2.log.info(`shape '${opts.shape}' already matches existing adapters [${current.join(", ")}]`);
713
+ } else {
714
+ if (missing.length > 0) {
715
+ const lines = [
716
+ `--shape ${opts.shape} requested but xera.config.ts has adapters: [${current.join(", ")}]`,
717
+ `Missing adapter(s): ${missing.join(", ")}`,
718
+ ``,
719
+ `init --update is non-destructive \u2014 it will NOT modify xera.config.ts or shared/auth-setup.ts.`,
720
+ `To complete the upgrade, hand-edit both files:`,
721
+ ``,
722
+ `1. In xera.config.ts, change \`adapters: [${current.map((a) => `'${a}'`).join(", ")}]\` to \`adapters: [${requested.map((a) => `'${a}'`).join(", ")}]\` and add this block inside defineConfig({...}):`,
723
+ ``
724
+ ];
725
+ for (const a of missing) {
726
+ lines.push(a === "http" ? renderHttpConfigSnippet(opts) : renderWebConfigSnippet(opts));
727
+ lines.push("");
728
+ }
729
+ lines.push(`2. In shared/auth-setup.ts, add this export:`);
730
+ lines.push("");
731
+ for (const a of missing) {
732
+ lines.push(a === "http" ? HTTP_AUTH_SETUP_SNIPPET : WEB_AUTH_SETUP_SNIPPET);
733
+ lines.push("");
734
+ }
735
+ lines.push(`3. Add ${missing.includes("http") ? `\`@xera-ai/http\`` : ""}${missing.includes("http") && missing.includes("web") ? " and " : ""}${missing.includes("web") ? `\`@xera-ai/web\`` : ""} to dependencies (re-run \`xera init --update\` after editing to bump versions), then run \`xera doctor\` to verify.`);
736
+ p2.log.warn(lines.join(`
737
+ `));
738
+ }
739
+ if (extra.length > 0) {
740
+ p2.log.warn(`--shape ${opts.shape} would remove adapter(s) [${extra.join(", ")}] from xera.config.ts, but init --update never removes config. Remove the block(s) by hand if intended.`);
741
+ }
742
+ }
743
+ }
744
+ } else if (hasShapeFlags) {
745
+ p2.log.warn("Shape-related flags (--au/--as/--hr/--su/--ro/--op/--auth-enabled) are ignored by init --update without --shape. To change shape, pass --shape <web|api|mixed> alongside.");
746
+ }
619
747
  p2.outro(pc3.green("Update complete. Run `xera doctor` to verify."));
620
748
  }
621
749
 
@@ -668,7 +796,38 @@ async function main() {
668
796
  cli.usage("<command> [options]");
669
797
  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) => {
670
798
  if (opts.update) {
671
- await initUpdateCommand({ yes: !!opts.yes });
799
+ const updateOpts = { yes: !!opts.yes };
800
+ if (opts.shape !== undefined) {
801
+ if (!VALID_SHAPES.includes(opts.shape)) {
802
+ console.error(pc4.red(`
803
+ error: --shape must be one of: ${VALID_SHAPES.join(", ")}
804
+ `));
805
+ process.exit(1);
806
+ }
807
+ updateOpts.shape = opts.shape;
808
+ }
809
+ if (opts.authStrategy !== undefined) {
810
+ if (!VALID_AUTH_STRATEGIES.includes(opts.authStrategy)) {
811
+ console.error(pc4.red(`
812
+ error: --auth-strategy must be one of: ${VALID_AUTH_STRATEGIES.join(", ")}
813
+ `));
814
+ process.exit(1);
815
+ }
816
+ updateOpts.authStrategy = opts.authStrategy;
817
+ }
818
+ if (opts.apiBaseUrl !== undefined)
819
+ updateOpts.apiBaseUrl = opts.apiBaseUrl;
820
+ if (opts.openapiPath !== undefined)
821
+ updateOpts.openapiPath = opts.openapiPath;
822
+ if (opts.httpRoles !== undefined)
823
+ updateOpts.httpRoles = opts.httpRoles;
824
+ if (opts.stagingUrl !== undefined)
825
+ updateOpts.stagingUrl = opts.stagingUrl;
826
+ if (opts.authEnabled !== undefined)
827
+ updateOpts.authEnabled = opts.authEnabled;
828
+ if (opts.roles !== undefined)
829
+ updateOpts.roles = opts.roles;
830
+ await initUpdateCommand(updateOpts);
672
831
  return;
673
832
  }
674
833
  const initOpts = { yes: !!opts.yes };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xera-ai/cli",
3
- "version": "0.11.6",
3
+ "version": "0.12.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.11.6",
19
- "@xera-ai/skills": "^0.11.6",
18
+ "@xera-ai/core": "^0.12.1",
19
+ "@xera-ai/skills": "^0.12.1",
20
20
  "@clack/prompts": "1.4.0",
21
21
  "cac": "7.0.0",
22
22
  "picocolors": "1.1.1"
@@ -2,6 +2,6 @@
2
2
  JIRA_EMAIL=
3
3
  JIRA_API_TOKEN=
4
4
 
5
- # Auth tokens for HTTP roles - set in .env.local (gitignored)
5
+ # Auth tokens for HTTP roles - set in .env (gitignored)
6
6
  {{#each httpRoles}}{{upper this}}_BEARER_TOKEN=
7
7
  {{/each}}
@@ -11,8 +11,8 @@ export default defineConfig({
11
11
  },
12
12
  },
13
13
  http: {
14
- baseUrl: { dev: '{{apiBaseUrl}}' },
15
- defaultEnv: 'dev',
14
+ baseUrl: { staging: '{{apiBaseUrl}}' },
15
+ defaultEnv: 'staging',
16
16
  {{#if openapiPath}}spec: '{{openapiPath}}',{{/if}}
17
17
  auth: {
18
18
  strategy: '{{authStrategy}}',
@@ -11,8 +11,8 @@ export default defineConfig({
11
11
  },
12
12
  },
13
13
  web: {
14
- baseUrl: { dev: '{{stagingUrl}}' },
15
- defaultEnv: 'dev',
14
+ baseUrl: { staging: '{{stagingUrl}}' },
15
+ defaultEnv: 'staging',
16
16
  {{#if authEnabled}}auth: {
17
17
  strategy: 'storageState',
18
18
  setupScript: './shared/auth-setup.ts',
@@ -23,8 +23,8 @@ export default defineConfig({
23
23
  },{{/if}}
24
24
  },
25
25
  http: {
26
- baseUrl: { dev: '{{apiBaseUrl}}' },
27
- defaultEnv: 'dev',
26
+ baseUrl: { staging: '{{apiBaseUrl}}' },
27
+ defaultEnv: 'staging',
28
28
  {{#if openapiPath}}spec: '{{openapiPath}}',{{/if}}
29
29
  auth: {
30
30
  strategy: '{{authStrategy}}',