@xera-ai/cli 0.8.1 → 0.9.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 +150 -79
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -266,10 +266,36 @@ 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();
|
|
285
|
+
if (!process.stdin.isTTY && !opts.yes) {
|
|
286
|
+
console.error(pc2.red(`
|
|
287
|
+
error: stdin is not a TTY \u2014 interactive prompts cannot run.
|
|
288
|
+
`));
|
|
289
|
+
console.error(` Pass ${pc2.cyan("-y / --yes")} and use flags to run non-interactively:
|
|
290
|
+
`);
|
|
291
|
+
console.error(` xera init -y --shape web --pk MYPROJ --ju https://myco.atlassian.net
|
|
292
|
+
`);
|
|
293
|
+
console.error(` Run ${pc2.cyan("xera init --help")} to see all available flags.
|
|
294
|
+
`);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
271
297
|
p.intro(pc2.cyan("xera \u2014 project setup"));
|
|
272
|
-
const shape = opts.shape
|
|
298
|
+
const shape = await prompt(opts.shape, opts.yes ? "web" : undefined, () => p.select({
|
|
273
299
|
message: "What kind of testing does this project do?",
|
|
274
300
|
initialValue: "web",
|
|
275
301
|
options: [
|
|
@@ -278,68 +304,37 @@ async function initCommand(opts) {
|
|
|
278
304
|
{ value: "mixed", label: "Both (some UI tickets, some API tickets, in one repo)" }
|
|
279
305
|
]
|
|
280
306
|
}));
|
|
281
|
-
if (typeof shape === "symbol") {
|
|
282
|
-
p.cancel("Aborted.");
|
|
283
|
-
process.exit(0);
|
|
284
|
-
}
|
|
285
307
|
const wantsWeb = shape === "web" || shape === "mixed";
|
|
286
308
|
const wantsHttp = shape === "api" || shape === "mixed";
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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({
|
|
309
|
+
const jiraBaseUrl = await prompt(opts.jiraBaseUrl, opts.yes ? "https://example.atlassian.net" : undefined, () => p.text({ message: "Jira workspace URL", placeholder: "https://x.atlassian.net" }));
|
|
310
|
+
const projectKeys = await prompt(opts.projectKeys, opts.yes ? "JIRA" : undefined, () => p.text({ message: "Jira project key(s) (comma-separated)", placeholder: "JIRA" }));
|
|
311
|
+
const storyField = await prompt(opts.storyField, opts.yes ? "description" : undefined, () => p.text({ message: "Jira field id for user story", initialValue: "description" }));
|
|
312
|
+
const acceptanceCriteriaField = await prompt(opts.acField, opts.yes ? "" : undefined, () => p.text({
|
|
313
|
+
message: "Jira field id for AC (leave empty if same as story)",
|
|
314
|
+
initialValue: ""
|
|
315
|
+
}));
|
|
316
|
+
let stagingUrl = "";
|
|
317
|
+
let authEnabled = false;
|
|
318
|
+
let roles = "";
|
|
319
|
+
if (wantsWeb) {
|
|
320
|
+
stagingUrl = await prompt(opts.stagingUrl, opts.yes ? "http://localhost:3000" : undefined, () => p.text({
|
|
311
321
|
message: "Web app staging URL",
|
|
312
322
|
placeholder: "https://staging.example.com"
|
|
313
|
-
})
|
|
314
|
-
authEnabled: () => p.confirm({
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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({
|
|
323
|
+
}));
|
|
324
|
+
authEnabled = await prompt(opts.authEnabled, opts.yes ? true : undefined, () => p.confirm({ message: "Does your app require login to test most pages?", initialValue: true }));
|
|
325
|
+
roles = await prompt(opts.roles, opts.yes ? "admin,regular" : undefined, () => p.text({ message: "Test user roles (comma-separated)", initialValue: "admin,regular" }));
|
|
326
|
+
}
|
|
327
|
+
let apiBaseUrl = "";
|
|
328
|
+
let openapiPath = "";
|
|
329
|
+
let authStrategy = "none";
|
|
330
|
+
let httpRoles = "";
|
|
331
|
+
if (wantsHttp) {
|
|
332
|
+
apiBaseUrl = await prompt(opts.apiBaseUrl, opts.yes ? "http://localhost:4111" : undefined, () => p.text({ message: "API base URL", placeholder: "https://api.staging.example.com" }));
|
|
333
|
+
openapiPath = await prompt(opts.openapiPath, opts.yes ? "./openapi.yaml" : undefined, () => p.text({
|
|
339
334
|
message: "OpenAPI spec path (relative or URL \u2014 leave empty to skip)",
|
|
340
335
|
initialValue: "./openapi.yaml"
|
|
341
|
-
})
|
|
342
|
-
authStrategy: () => p.select({
|
|
336
|
+
}));
|
|
337
|
+
authStrategy = await prompt(opts.authStrategy, opts.yes ? "bearer" : undefined, () => p.select({
|
|
343
338
|
message: "API auth strategy",
|
|
344
339
|
initialValue: "bearer",
|
|
345
340
|
options: [
|
|
@@ -349,29 +344,24 @@ async function initCommand(opts) {
|
|
|
349
344
|
{ value: "oauth-cc", label: "OAuth client_credentials" },
|
|
350
345
|
{ value: "none", label: "None / public API" }
|
|
351
346
|
]
|
|
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
|
-
});
|
|
347
|
+
}));
|
|
348
|
+
httpRoles = await prompt(opts.httpRoles, opts.yes ? "user" : undefined, () => p.text({ message: "HTTP roles (comma-separated)", initialValue: "user" }));
|
|
349
|
+
}
|
|
360
350
|
const vars = {
|
|
361
351
|
shape,
|
|
362
352
|
wantsWeb,
|
|
363
353
|
wantsHttp,
|
|
364
|
-
jiraBaseUrl
|
|
365
|
-
projectKeys:
|
|
366
|
-
storyField
|
|
367
|
-
acceptanceCriteriaField
|
|
368
|
-
stagingUrl
|
|
369
|
-
authEnabled
|
|
370
|
-
roles:
|
|
371
|
-
apiBaseUrl
|
|
372
|
-
openapiPath
|
|
373
|
-
authStrategy
|
|
374
|
-
httpRoles:
|
|
354
|
+
jiraBaseUrl,
|
|
355
|
+
projectKeys: projectKeys.split(",").map((s) => s.trim()).filter(Boolean),
|
|
356
|
+
storyField,
|
|
357
|
+
acceptanceCriteriaField,
|
|
358
|
+
stagingUrl,
|
|
359
|
+
authEnabled,
|
|
360
|
+
roles: roles ? roles.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
361
|
+
apiBaseUrl,
|
|
362
|
+
openapiPath,
|
|
363
|
+
authStrategy,
|
|
364
|
+
httpRoles: httpRoles ? httpRoles.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
375
365
|
authKey: generateKey()
|
|
376
366
|
};
|
|
377
367
|
const configTmpl = shape === "web" ? "xera.config.ts.tmpl" : shape === "api" ? "http-xera.config.ts.tmpl" : "mixed-xera.config.ts.tmpl";
|
|
@@ -585,11 +575,50 @@ async function initUpdateCommand(_opts) {
|
|
|
585
575
|
var require4 = createRequire3(import.meta.url);
|
|
586
576
|
var VERSION = require4("../package.json").version;
|
|
587
577
|
var VALID_SHAPES = ["web", "api", "mixed"];
|
|
578
|
+
var VALID_AUTH_STRATEGIES = ["bearer", "apiKey", "basic", "oauth-cc", "none"];
|
|
579
|
+
var KNOWN_COMMANDS = ["init", "doctor"];
|
|
580
|
+
function levenshtein(a, b) {
|
|
581
|
+
const m = a.length;
|
|
582
|
+
const n = b.length;
|
|
583
|
+
const dp = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0));
|
|
584
|
+
for (let i = 1;i <= m; i++) {
|
|
585
|
+
for (let j = 1;j <= n; j++) {
|
|
586
|
+
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return dp[m][n];
|
|
590
|
+
}
|
|
591
|
+
function didYouMean(input) {
|
|
592
|
+
let best;
|
|
593
|
+
let bestDist = Number.POSITIVE_INFINITY;
|
|
594
|
+
for (const cmd of KNOWN_COMMANDS) {
|
|
595
|
+
const d = levenshtein(input.toLowerCase(), cmd);
|
|
596
|
+
if (d < bestDist) {
|
|
597
|
+
bestDist = d;
|
|
598
|
+
best = cmd;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return bestDist <= 3 ? best : undefined;
|
|
602
|
+
}
|
|
603
|
+
function unknownCommand(input) {
|
|
604
|
+
console.error(pc4.red(`
|
|
605
|
+
error: Unknown command '${input}'
|
|
606
|
+
`));
|
|
607
|
+
const suggestion = didYouMean(input);
|
|
608
|
+
if (suggestion) {
|
|
609
|
+
console.error(` Did you mean ${pc4.cyan(suggestion)}?
|
|
610
|
+
`);
|
|
611
|
+
}
|
|
612
|
+
console.error(` Run ${pc4.cyan("xera --help")} to see available commands.
|
|
613
|
+
`);
|
|
614
|
+
process.exit(1);
|
|
615
|
+
}
|
|
588
616
|
async function main() {
|
|
589
617
|
const cli = cac("xera");
|
|
590
618
|
cli.help();
|
|
591
619
|
cli.version(VERSION);
|
|
592
|
-
cli.
|
|
620
|
+
cli.usage("<command> [options]");
|
|
621
|
+
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
622
|
if (opts.update) {
|
|
594
623
|
await initUpdateCommand({ yes: !!opts.yes });
|
|
595
624
|
return;
|
|
@@ -597,22 +626,64 @@ async function main() {
|
|
|
597
626
|
const initOpts = { yes: !!opts.yes };
|
|
598
627
|
if (opts.shape !== undefined) {
|
|
599
628
|
if (!VALID_SHAPES.includes(opts.shape)) {
|
|
600
|
-
console.error(pc4.red(`
|
|
629
|
+
console.error(pc4.red(`
|
|
630
|
+
error: --shape must be one of: ${VALID_SHAPES.join(", ")}
|
|
631
|
+
`));
|
|
601
632
|
process.exit(1);
|
|
602
633
|
}
|
|
603
634
|
initOpts.shape = opts.shape;
|
|
604
635
|
}
|
|
636
|
+
if (opts.authStrategy !== undefined) {
|
|
637
|
+
if (!VALID_AUTH_STRATEGIES.includes(opts.authStrategy)) {
|
|
638
|
+
console.error(pc4.red(`
|
|
639
|
+
error: --auth-strategy must be one of: ${VALID_AUTH_STRATEGIES.join(", ")}
|
|
640
|
+
`));
|
|
641
|
+
process.exit(1);
|
|
642
|
+
}
|
|
643
|
+
initOpts.authStrategy = opts.authStrategy;
|
|
644
|
+
}
|
|
645
|
+
if (opts.jiraBaseUrl !== undefined)
|
|
646
|
+
initOpts.jiraBaseUrl = opts.jiraBaseUrl;
|
|
647
|
+
if (opts.projectKeys !== undefined)
|
|
648
|
+
initOpts.projectKeys = opts.projectKeys;
|
|
649
|
+
if (opts.storyField !== undefined)
|
|
650
|
+
initOpts.storyField = opts.storyField;
|
|
651
|
+
if (opts.acField !== undefined)
|
|
652
|
+
initOpts.acField = opts.acField;
|
|
653
|
+
if (opts.stagingUrl !== undefined)
|
|
654
|
+
initOpts.stagingUrl = opts.stagingUrl;
|
|
655
|
+
if (opts.authEnabled !== undefined)
|
|
656
|
+
initOpts.authEnabled = opts.authEnabled;
|
|
657
|
+
if (opts.roles !== undefined)
|
|
658
|
+
initOpts.roles = opts.roles;
|
|
659
|
+
if (opts.apiBaseUrl !== undefined)
|
|
660
|
+
initOpts.apiBaseUrl = opts.apiBaseUrl;
|
|
661
|
+
if (opts.openapiPath !== undefined)
|
|
662
|
+
initOpts.openapiPath = opts.openapiPath;
|
|
663
|
+
if (opts.httpRoles !== undefined)
|
|
664
|
+
initOpts.httpRoles = opts.httpRoles;
|
|
605
665
|
await initCommand(initOpts);
|
|
606
666
|
});
|
|
607
667
|
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) => {
|
|
608
668
|
const exit = await doctorCommand(opts);
|
|
609
669
|
process.exit(exit);
|
|
610
670
|
});
|
|
671
|
+
const rawArgs = process.argv.slice(2);
|
|
672
|
+
if (rawArgs.length === 0) {
|
|
673
|
+
cli.outputHelp();
|
|
674
|
+
process.exit(0);
|
|
675
|
+
}
|
|
676
|
+
const firstArg = rawArgs[0];
|
|
677
|
+
if (!firstArg.startsWith("-") && !KNOWN_COMMANDS.includes(firstArg)) {
|
|
678
|
+
unknownCommand(firstArg);
|
|
679
|
+
}
|
|
611
680
|
try {
|
|
612
681
|
cli.parse(process.argv, { run: false });
|
|
613
682
|
await cli.runMatchedCommand();
|
|
614
683
|
} catch (e) {
|
|
615
|
-
console.error(pc4.red(`
|
|
684
|
+
console.error(pc4.red(`
|
|
685
|
+
error: ${e.message}
|
|
686
|
+
`));
|
|
616
687
|
process.exit(1);
|
|
617
688
|
}
|
|
618
689
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xera-ai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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.9.1",
|
|
19
|
+
"@xera-ai/skills": "^0.9.1",
|
|
20
20
|
"@clack/prompts": "1.4.0",
|
|
21
21
|
"cac": "7.0.0",
|
|
22
22
|
"picocolors": "1.1.1"
|