@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)
|
|
500
|
-
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
19
|
-
"@xera-ai/skills": "^0.12.
|
|
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}}',
|