@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)
|
|
498
|
-
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
19
|
-
"@xera-ai/skills": "^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"
|
|
@@ -11,8 +11,8 @@ export default defineConfig({
|
|
|
11
11
|
},
|
|
12
12
|
},
|
|
13
13
|
http: {
|
|
14
|
-
baseUrl: {
|
|
15
|
-
defaultEnv: '
|
|
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: {
|
|
15
|
-
defaultEnv: '
|
|
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: {
|
|
27
|
-
defaultEnv: '
|
|
26
|
+
baseUrl: { staging: '{{apiBaseUrl}}' },
|
|
27
|
+
defaultEnv: 'staging',
|
|
28
28
|
{{#if openapiPath}}spec: '{{openapiPath}}',{{/if}}
|
|
29
29
|
auth: {
|
|
30
30
|
strategy: '{{authStrategy}}',
|