@xera-ai/cli 0.12.0 → 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
@@ -496,22 +496,25 @@ async function initCommand(opts) {
496
496
  writeFileSync2(pkgPath, JSON.stringify(pkg, null, 2));
497
497
  const nextSteps = shape === "api" ? `
498
498
  Next:
499
- 1) Set your auth credentials in .env.local:
500
- 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=...
501
502
  2) Run pre-authentication:
502
503
  bun run xera:auth-setup
503
504
  3) Start testing:
504
505
  Open Claude Code in this directory and run: /xera-run <TICKET>
505
506
  ` : shape === "mixed" ? `
506
507
  Next:
507
- 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
508
510
  2) Run pre-authentication:
509
511
  bun run xera:auth-setup
510
512
  3) Start testing:
511
513
  Open Claude Code in this directory and run: /xera-run <TICKET>
512
514
  ` : `
513
515
  Next:
514
- 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
515
518
  2) Run pre-authentication:
516
519
  bun run xera:auth-setup
517
520
  3) Start testing:
@@ -536,7 +539,85 @@ import * as p2 from "@clack/prompts";
536
539
  import pc3 from "picocolors";
537
540
  var require3 = createRequire2(import.meta.url);
538
541
  var CLI_VERSION2 = require3("../package.json").version;
539
- 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) {
540
621
  const cwd = process.cwd();
541
622
  p2.intro(pc3.cyan("xera init --update"));
542
623
  const pkgPath = join4(cwd, "package.json");
@@ -618,6 +699,51 @@ async function initUpdateCommand(_opts) {
618
699
  p2.log.warn(`kept local ${name}`);
619
700
  }
620
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
+ }
621
747
  p2.outro(pc3.green("Update complete. Run `xera doctor` to verify."));
622
748
  }
623
749
 
@@ -670,7 +796,38 @@ async function main() {
670
796
  cli.usage("<command> [options]");
671
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) => {
672
798
  if (opts.update) {
673
- 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);
674
831
  return;
675
832
  }
676
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.12.0",
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.12.0",
19
- "@xera-ai/skills": "^0.12.0",
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}}',