@xera-ai/cli 0.8.0 → 0.9.0
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 +84 -77
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -266,10 +266,24 @@ 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();
|
|
271
285
|
p.intro(pc2.cyan("xera \u2014 project setup"));
|
|
272
|
-
const shape = opts.shape
|
|
286
|
+
const shape = await prompt(opts.shape, opts.yes ? "web" : undefined, () => p.select({
|
|
273
287
|
message: "What kind of testing does this project do?",
|
|
274
288
|
initialValue: "web",
|
|
275
289
|
options: [
|
|
@@ -278,68 +292,37 @@ async function initCommand(opts) {
|
|
|
278
292
|
{ value: "mixed", label: "Both (some UI tickets, some API tickets, in one repo)" }
|
|
279
293
|
]
|
|
280
294
|
}));
|
|
281
|
-
if (typeof shape === "symbol") {
|
|
282
|
-
p.cancel("Aborted.");
|
|
283
|
-
process.exit(0);
|
|
284
|
-
}
|
|
285
295
|
const wantsWeb = shape === "web" || shape === "mixed";
|
|
286
296
|
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({
|
|
297
|
+
const jiraBaseUrl = await prompt(opts.jiraBaseUrl, opts.yes ? "https://example.atlassian.net" : undefined, () => p.text({ message: "Jira workspace URL", placeholder: "https://x.atlassian.net" }));
|
|
298
|
+
const projectKeys = await prompt(opts.projectKeys, opts.yes ? "JIRA" : undefined, () => p.text({ message: "Jira project key(s) (comma-separated)", placeholder: "JIRA" }));
|
|
299
|
+
const storyField = await prompt(opts.storyField, opts.yes ? "description" : undefined, () => p.text({ message: "Jira field id for user story", initialValue: "description" }));
|
|
300
|
+
const acceptanceCriteriaField = await prompt(opts.acField, opts.yes ? "" : undefined, () => p.text({
|
|
301
|
+
message: "Jira field id for AC (leave empty if same as story)",
|
|
302
|
+
initialValue: ""
|
|
303
|
+
}));
|
|
304
|
+
let stagingUrl = "";
|
|
305
|
+
let authEnabled = false;
|
|
306
|
+
let roles = "";
|
|
307
|
+
if (wantsWeb) {
|
|
308
|
+
stagingUrl = await prompt(opts.stagingUrl, opts.yes ? "http://localhost:3000" : undefined, () => p.text({
|
|
311
309
|
message: "Web app staging URL",
|
|
312
310
|
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({
|
|
311
|
+
}));
|
|
312
|
+
authEnabled = await prompt(opts.authEnabled, opts.yes ? true : undefined, () => p.confirm({ message: "Does your app require login to test most pages?", initialValue: true }));
|
|
313
|
+
roles = await prompt(opts.roles, opts.yes ? "admin,regular" : undefined, () => p.text({ message: "Test user roles (comma-separated)", initialValue: "admin,regular" }));
|
|
314
|
+
}
|
|
315
|
+
let apiBaseUrl = "";
|
|
316
|
+
let openapiPath = "";
|
|
317
|
+
let authStrategy = "none";
|
|
318
|
+
let httpRoles = "";
|
|
319
|
+
if (wantsHttp) {
|
|
320
|
+
apiBaseUrl = await prompt(opts.apiBaseUrl, opts.yes ? "http://localhost:4111" : undefined, () => p.text({ message: "API base URL", placeholder: "https://api.staging.example.com" }));
|
|
321
|
+
openapiPath = await prompt(opts.openapiPath, opts.yes ? "./openapi.yaml" : undefined, () => p.text({
|
|
339
322
|
message: "OpenAPI spec path (relative or URL \u2014 leave empty to skip)",
|
|
340
323
|
initialValue: "./openapi.yaml"
|
|
341
|
-
})
|
|
342
|
-
authStrategy: () => p.select({
|
|
324
|
+
}));
|
|
325
|
+
authStrategy = await prompt(opts.authStrategy, opts.yes ? "bearer" : undefined, () => p.select({
|
|
343
326
|
message: "API auth strategy",
|
|
344
327
|
initialValue: "bearer",
|
|
345
328
|
options: [
|
|
@@ -349,29 +332,24 @@ async function initCommand(opts) {
|
|
|
349
332
|
{ value: "oauth-cc", label: "OAuth client_credentials" },
|
|
350
333
|
{ value: "none", label: "None / public API" }
|
|
351
334
|
]
|
|
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
|
-
});
|
|
335
|
+
}));
|
|
336
|
+
httpRoles = await prompt(opts.httpRoles, opts.yes ? "user" : undefined, () => p.text({ message: "HTTP roles (comma-separated)", initialValue: "user" }));
|
|
337
|
+
}
|
|
360
338
|
const vars = {
|
|
361
339
|
shape,
|
|
362
340
|
wantsWeb,
|
|
363
341
|
wantsHttp,
|
|
364
|
-
jiraBaseUrl
|
|
365
|
-
projectKeys:
|
|
366
|
-
storyField
|
|
367
|
-
acceptanceCriteriaField
|
|
368
|
-
stagingUrl
|
|
369
|
-
authEnabled
|
|
370
|
-
roles:
|
|
371
|
-
apiBaseUrl
|
|
372
|
-
openapiPath
|
|
373
|
-
authStrategy
|
|
374
|
-
httpRoles:
|
|
342
|
+
jiraBaseUrl,
|
|
343
|
+
projectKeys: projectKeys.split(",").map((s) => s.trim()).filter(Boolean),
|
|
344
|
+
storyField,
|
|
345
|
+
acceptanceCriteriaField,
|
|
346
|
+
stagingUrl,
|
|
347
|
+
authEnabled,
|
|
348
|
+
roles: roles ? roles.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
349
|
+
apiBaseUrl,
|
|
350
|
+
openapiPath,
|
|
351
|
+
authStrategy,
|
|
352
|
+
httpRoles: httpRoles ? httpRoles.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
375
353
|
authKey: generateKey()
|
|
376
354
|
};
|
|
377
355
|
const configTmpl = shape === "web" ? "xera.config.ts.tmpl" : shape === "api" ? "http-xera.config.ts.tmpl" : "mixed-xera.config.ts.tmpl";
|
|
@@ -585,11 +563,13 @@ async function initUpdateCommand(_opts) {
|
|
|
585
563
|
var require4 = createRequire3(import.meta.url);
|
|
586
564
|
var VERSION = require4("../package.json").version;
|
|
587
565
|
var VALID_SHAPES = ["web", "api", "mixed"];
|
|
566
|
+
var VALID_AUTH_STRATEGIES = ["bearer", "apiKey", "basic", "oauth-cc", "none"];
|
|
588
567
|
async function main() {
|
|
589
568
|
const cli = cac("xera");
|
|
590
569
|
cli.help();
|
|
591
570
|
cli.version(VERSION);
|
|
592
|
-
cli.
|
|
571
|
+
cli.usage("<command> [options]");
|
|
572
|
+
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
573
|
if (opts.update) {
|
|
594
574
|
await initUpdateCommand({ yes: !!opts.yes });
|
|
595
575
|
return;
|
|
@@ -602,6 +582,33 @@ async function main() {
|
|
|
602
582
|
}
|
|
603
583
|
initOpts.shape = opts.shape;
|
|
604
584
|
}
|
|
585
|
+
if (opts.authStrategy !== undefined) {
|
|
586
|
+
if (!VALID_AUTH_STRATEGIES.includes(opts.authStrategy)) {
|
|
587
|
+
console.error(pc4.red(`[xera] --auth-strategy must be one of: ${VALID_AUTH_STRATEGIES.join(", ")}`));
|
|
588
|
+
process.exit(1);
|
|
589
|
+
}
|
|
590
|
+
initOpts.authStrategy = opts.authStrategy;
|
|
591
|
+
}
|
|
592
|
+
if (opts.jiraBaseUrl !== undefined)
|
|
593
|
+
initOpts.jiraBaseUrl = opts.jiraBaseUrl;
|
|
594
|
+
if (opts.projectKeys !== undefined)
|
|
595
|
+
initOpts.projectKeys = opts.projectKeys;
|
|
596
|
+
if (opts.storyField !== undefined)
|
|
597
|
+
initOpts.storyField = opts.storyField;
|
|
598
|
+
if (opts.acField !== undefined)
|
|
599
|
+
initOpts.acField = opts.acField;
|
|
600
|
+
if (opts.stagingUrl !== undefined)
|
|
601
|
+
initOpts.stagingUrl = opts.stagingUrl;
|
|
602
|
+
if (opts.authEnabled !== undefined)
|
|
603
|
+
initOpts.authEnabled = opts.authEnabled;
|
|
604
|
+
if (opts.roles !== undefined)
|
|
605
|
+
initOpts.roles = opts.roles;
|
|
606
|
+
if (opts.apiBaseUrl !== undefined)
|
|
607
|
+
initOpts.apiBaseUrl = opts.apiBaseUrl;
|
|
608
|
+
if (opts.openapiPath !== undefined)
|
|
609
|
+
initOpts.openapiPath = opts.openapiPath;
|
|
610
|
+
if (opts.httpRoles !== undefined)
|
|
611
|
+
initOpts.httpRoles = opts.httpRoles;
|
|
605
612
|
await initCommand(initOpts);
|
|
606
613
|
});
|
|
607
614
|
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) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xera-ai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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.0",
|
|
19
|
+
"@xera-ai/skills": "^0.9.0",
|
|
20
20
|
"@clack/prompts": "1.4.0",
|
|
21
21
|
"cac": "7.0.0",
|
|
22
22
|
"picocolors": "1.1.1"
|