@xera-ai/cli 0.14.0 → 0.15.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 +190 -46
- package/package.json +3 -3
- package/templates/samples/http/SAMPLE-HTTP-001/meta.json.tmpl +7 -0
- package/templates/samples/http/SAMPLE-HTTP-001/story.md +38 -0
- package/templates/samples/http/SAMPLE-HTTP-001/test.feature +20 -0
- package/templates/samples/web/SAMPLE-001/meta.json.tmpl +7 -0
- package/templates/samples/web/SAMPLE-001/story.md +29 -0
- package/templates/samples/web/SAMPLE-001/test.feature +16 -0
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ var __require = import.meta.require;
|
|
|
4
4
|
// src/index.ts
|
|
5
5
|
import { createRequire as createRequire3 } from "module";
|
|
6
6
|
import { cac } from "cac";
|
|
7
|
-
import
|
|
7
|
+
import pc5 from "picocolors";
|
|
8
8
|
|
|
9
9
|
// src/commands/doctor.ts
|
|
10
10
|
import { existsSync as existsSync6 } from "fs";
|
|
@@ -608,9 +608,9 @@ async function doctorCommand(opts) {
|
|
|
608
608
|
}
|
|
609
609
|
|
|
610
610
|
// src/commands/init.ts
|
|
611
|
-
import { appendFileSync, existsSync as
|
|
611
|
+
import { appendFileSync, existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
612
612
|
import { createRequire } from "module";
|
|
613
|
-
import { join as
|
|
613
|
+
import { join as join8 } from "path";
|
|
614
614
|
import * as p from "@clack/prompts";
|
|
615
615
|
import { generateKey } from "@xera-ai/core";
|
|
616
616
|
import pc2 from "picocolors";
|
|
@@ -644,6 +644,10 @@ async function resolveEditors(opts) {
|
|
|
644
644
|
return [...ALL_EDITORS];
|
|
645
645
|
}
|
|
646
646
|
|
|
647
|
+
// src/samples.ts
|
|
648
|
+
import { copyFileSync, existsSync as existsSync7, mkdirSync as mkdirSync5, readdirSync as readdirSync2, rmSync } from "fs";
|
|
649
|
+
import { join as join7 } from "path";
|
|
650
|
+
|
|
647
651
|
// src/scaffold.ts
|
|
648
652
|
import { mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
649
653
|
import { dirname as dirname4, join as join6 } from "path";
|
|
@@ -669,6 +673,62 @@ function scaffoldFile(targetPath, templateName, vars) {
|
|
|
669
673
|
mkdirSync4(dirname4(targetPath), { recursive: true });
|
|
670
674
|
writeFileSync4(targetPath, render(tmpl, vars));
|
|
671
675
|
}
|
|
676
|
+
var TEMPLATE_DIR = TEMPLATE_ROOT;
|
|
677
|
+
|
|
678
|
+
// src/samples.ts
|
|
679
|
+
var SAMPLES_ROOT = join7(TEMPLATE_DIR, "samples");
|
|
680
|
+
var WEB_SAMPLE = {
|
|
681
|
+
id: "SAMPLE-001",
|
|
682
|
+
templateDir: join7(SAMPLES_ROOT, "web", "SAMPLE-001")
|
|
683
|
+
};
|
|
684
|
+
var HTTP_SAMPLE = {
|
|
685
|
+
id: "SAMPLE-HTTP-001",
|
|
686
|
+
templateDir: join7(SAMPLES_ROOT, "http", "SAMPLE-HTTP-001")
|
|
687
|
+
};
|
|
688
|
+
function samplesForShape(shape) {
|
|
689
|
+
if (shape === "web")
|
|
690
|
+
return [WEB_SAMPLE];
|
|
691
|
+
if (shape === "api")
|
|
692
|
+
return [HTTP_SAMPLE];
|
|
693
|
+
return [WEB_SAMPLE, HTTP_SAMPLE];
|
|
694
|
+
}
|
|
695
|
+
function allSamples() {
|
|
696
|
+
return [WEB_SAMPLE, HTTP_SAMPLE];
|
|
697
|
+
}
|
|
698
|
+
function scaffoldSample(cwd, sample, vars) {
|
|
699
|
+
const written = [];
|
|
700
|
+
const skipped = [];
|
|
701
|
+
const targetDir = join7(cwd, ".xera", sample.id);
|
|
702
|
+
mkdirSync5(targetDir, { recursive: true });
|
|
703
|
+
for (const entry of readdirSync2(sample.templateDir)) {
|
|
704
|
+
const src = join7(sample.templateDir, entry);
|
|
705
|
+
const isTmpl = entry.endsWith(".tmpl");
|
|
706
|
+
const outName = isTmpl ? entry.replace(/\.tmpl$/, "") : entry;
|
|
707
|
+
const dest = join7(targetDir, outName);
|
|
708
|
+
if (existsSync7(dest)) {
|
|
709
|
+
skipped.push(dest);
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
if (isTmpl) {
|
|
713
|
+
const rel = src.slice(TEMPLATE_DIR.length + 1);
|
|
714
|
+
scaffoldFile(dest, rel, vars);
|
|
715
|
+
} else {
|
|
716
|
+
copyFileSync(src, dest);
|
|
717
|
+
}
|
|
718
|
+
written.push(dest);
|
|
719
|
+
}
|
|
720
|
+
return { written, skipped };
|
|
721
|
+
}
|
|
722
|
+
function removeSample(cwd, sample) {
|
|
723
|
+
const dir = join7(cwd, ".xera", sample.id);
|
|
724
|
+
if (!existsSync7(dir))
|
|
725
|
+
return false;
|
|
726
|
+
rmSync(dir, { recursive: true, force: true });
|
|
727
|
+
return true;
|
|
728
|
+
}
|
|
729
|
+
function detectInstalledSamples(cwd) {
|
|
730
|
+
return allSamples().filter((s) => existsSync7(join7(cwd, ".xera", s.id)));
|
|
731
|
+
}
|
|
672
732
|
|
|
673
733
|
// src/commands/init.ts
|
|
674
734
|
var require2 = createRequire(import.meta.url);
|
|
@@ -772,26 +832,38 @@ async function initCommand(opts) {
|
|
|
772
832
|
authKey: generateKey()
|
|
773
833
|
};
|
|
774
834
|
const configTmpl = shape === "web" ? "xera.config.ts.tmpl" : shape === "api" ? "http-xera.config.ts.tmpl" : "mixed-xera.config.ts.tmpl";
|
|
775
|
-
scaffoldFile(
|
|
835
|
+
scaffoldFile(join8(cwd, "xera.config.ts"), configTmpl, vars);
|
|
776
836
|
const pwTmpl = shape === "api" ? "http-playwright.config.ts.tmpl" : "playwright.config.ts.tmpl";
|
|
777
|
-
scaffoldFile(
|
|
778
|
-
scaffoldFile(
|
|
837
|
+
scaffoldFile(join8(cwd, "playwright.config.ts"), pwTmpl, vars);
|
|
838
|
+
scaffoldFile(join8(cwd, "tsconfig.json"), "tsconfig.json.tmpl", vars);
|
|
779
839
|
if (shape === "api") {
|
|
780
|
-
scaffoldFile(
|
|
840
|
+
scaffoldFile(join8(cwd, ".env.example"), "http-env.example.tmpl", vars);
|
|
781
841
|
} else {
|
|
782
|
-
scaffoldFile(
|
|
842
|
+
scaffoldFile(join8(cwd, ".env.example"), "env.example.tmpl", vars);
|
|
783
843
|
}
|
|
784
844
|
if (wantsWeb || wantsHttp) {
|
|
785
|
-
scaffoldFile(
|
|
845
|
+
scaffoldFile(join8(cwd, "shared/auth-setup.ts"), "auth-setup.ts.tmpl", vars);
|
|
786
846
|
}
|
|
787
|
-
scaffoldFile(
|
|
847
|
+
scaffoldFile(join8(cwd, ".github/workflows/xera-graph.yml"), "xera-graph.yml.template", vars);
|
|
788
848
|
if (wantsHttp && vars.openapiPath && !vars.openapiPath.startsWith("http")) {
|
|
789
|
-
const openapiTarget =
|
|
790
|
-
if (!
|
|
849
|
+
const openapiTarget = join8(cwd, vars.openapiPath);
|
|
850
|
+
if (!existsSync8(openapiTarget)) {
|
|
791
851
|
scaffoldFile(openapiTarget, "openapi.yaml.tmpl", vars);
|
|
792
852
|
}
|
|
793
853
|
}
|
|
794
|
-
|
|
854
|
+
if (opts.samples) {
|
|
855
|
+
const sampleVars = { ...vars, cliVersion: CLI_VERSION };
|
|
856
|
+
for (const sample of samplesForShape(shape)) {
|
|
857
|
+
const { written, skipped } = scaffoldSample(cwd, sample, sampleVars);
|
|
858
|
+
if (written.length > 0) {
|
|
859
|
+
p.log.success(`scaffolded sample ${sample.id} (${written.length} files)`);
|
|
860
|
+
}
|
|
861
|
+
if (skipped.length > 0) {
|
|
862
|
+
p.log.info(`sample ${sample.id}: skipped ${skipped.length} existing file(s)`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
const gitignorePath = join8(cwd, ".gitignore");
|
|
795
867
|
const gitignoreAdditions = [
|
|
796
868
|
"",
|
|
797
869
|
"# xera",
|
|
@@ -803,7 +875,7 @@ async function initCommand(opts) {
|
|
|
803
875
|
"node_modules/"
|
|
804
876
|
].join(`
|
|
805
877
|
`);
|
|
806
|
-
if (
|
|
878
|
+
if (existsSync8(gitignorePath)) {
|
|
807
879
|
const current = readFileSync4(gitignorePath, "utf8");
|
|
808
880
|
if (!current.includes("# xera"))
|
|
809
881
|
appendFileSync(gitignorePath, gitignoreAdditions);
|
|
@@ -833,14 +905,14 @@ async function initCommand(opts) {
|
|
|
833
905
|
}
|
|
834
906
|
});
|
|
835
907
|
const skillsPkgPath = require2.resolve("@xera-ai/skills/package.json");
|
|
836
|
-
const skillsSrcDir =
|
|
908
|
+
const skillsSrcDir = join8(skillsPkgPath, "..");
|
|
837
909
|
const SKILL_IGNORE = new Set(["package.json", "version.json", "CHANGELOG.md"]);
|
|
838
|
-
for (const name of
|
|
910
|
+
for (const name of readdirSync3(skillsSrcDir)) {
|
|
839
911
|
if (SKILL_IGNORE.has(name))
|
|
840
912
|
continue;
|
|
841
913
|
if (!name.endsWith(".md"))
|
|
842
914
|
continue;
|
|
843
|
-
const raw = readFileSync4(
|
|
915
|
+
const raw = readFileSync4(join8(skillsSrcDir, name), "utf8");
|
|
844
916
|
const { frontmatter, body } = parseFrontmatter(raw);
|
|
845
917
|
const base = name.replace(/\.md$/, "");
|
|
846
918
|
const skillInput = { base, body, frontmatter };
|
|
@@ -850,8 +922,8 @@ async function initCommand(opts) {
|
|
|
850
922
|
adapter.scaffoldCommand?.(cwd, skillInput);
|
|
851
923
|
}
|
|
852
924
|
}
|
|
853
|
-
const pkgPath =
|
|
854
|
-
const pkg =
|
|
925
|
+
const pkgPath = join8(cwd, "package.json");
|
|
926
|
+
const pkg = existsSync8(pkgPath) ? JSON.parse(readFileSync4(pkgPath, "utf8")) : { name: "xera-project", private: true, type: "module" };
|
|
855
927
|
pkg.scripts = pkg.scripts ?? {};
|
|
856
928
|
pkg.scripts["xera:fetch"] = "xera-internal fetch";
|
|
857
929
|
pkg.scripts["xera:validate-feature"] = "xera-internal validate-feature";
|
|
@@ -874,6 +946,8 @@ async function initCommand(opts) {
|
|
|
874
946
|
pkg.scripts["xera:impact-prepare"] = "xera-internal impact-prepare";
|
|
875
947
|
pkg.scripts["xera:heal-prepare"] = "xera-internal heal-prepare";
|
|
876
948
|
pkg.scripts["xera:disputes"] = "xera-internal disputes";
|
|
949
|
+
if (wantsHttp)
|
|
950
|
+
pkg.scripts["xera:openapi-resolve"] = "xera-internal openapi-resolve";
|
|
877
951
|
pkg.scripts["xera:explore-prepare"] = "xera-internal explore-prepare";
|
|
878
952
|
pkg.scripts["xera:explore-finalize"] = "xera-internal explore-finalize";
|
|
879
953
|
pkg.dependencies = pkg.dependencies ?? {};
|
|
@@ -924,28 +998,34 @@ Next:
|
|
|
924
998
|
3) Start testing:
|
|
925
999
|
${editorLines}
|
|
926
1000
|
`;
|
|
927
|
-
|
|
1001
|
+
const sampleIds = opts.samples ? samplesForShape(shape).map((s) => s.id) : [];
|
|
1002
|
+
const sampleHint = sampleIds.length > 0 ? `
|
|
1003
|
+
Sample ticket(s) scaffolded \u2014 try it out:
|
|
1004
|
+
${sampleIds.map((id) => ` /xera-run ${id}`).join(`
|
|
1005
|
+
`)}
|
|
1006
|
+
Remove later with: xera samples remove` : "";
|
|
1007
|
+
p.note((nextSteps.trim() + sampleHint).trim(), "Next steps");
|
|
928
1008
|
p.outro(pc2.green("xera initialized!"));
|
|
929
1009
|
}
|
|
930
1010
|
|
|
931
1011
|
// src/commands/init-update.ts
|
|
932
1012
|
import {
|
|
933
|
-
copyFileSync,
|
|
934
|
-
existsSync as
|
|
935
|
-
mkdirSync as
|
|
936
|
-
readdirSync as
|
|
1013
|
+
copyFileSync as copyFileSync2,
|
|
1014
|
+
existsSync as existsSync9,
|
|
1015
|
+
mkdirSync as mkdirSync6,
|
|
1016
|
+
readdirSync as readdirSync4,
|
|
937
1017
|
readFileSync as readFileSync5,
|
|
938
1018
|
writeFileSync as writeFileSync6
|
|
939
1019
|
} from "fs";
|
|
940
1020
|
import { createRequire as createRequire2 } from "module";
|
|
941
|
-
import { join as
|
|
1021
|
+
import { join as join9 } from "path";
|
|
942
1022
|
import * as p2 from "@clack/prompts";
|
|
943
1023
|
import pc3 from "picocolors";
|
|
944
1024
|
var require3 = createRequire2(import.meta.url);
|
|
945
1025
|
var CLI_VERSION2 = require3("../package.json").version;
|
|
946
1026
|
function detectAdaptersFromConfig(cwd) {
|
|
947
|
-
const configPath =
|
|
948
|
-
if (!
|
|
1027
|
+
const configPath = join9(cwd, "xera.config.ts");
|
|
1028
|
+
if (!existsSync9(configPath))
|
|
949
1029
|
return null;
|
|
950
1030
|
const cfg = readFileSync5(configPath, "utf8");
|
|
951
1031
|
const m = cfg.match(/adapters:\s*\[([^\]]+)\]/);
|
|
@@ -1024,8 +1104,8 @@ export const web = defineAuthSetup(async (page, _role, creds) => {
|
|
|
1024
1104
|
async function initUpdateCommand(opts) {
|
|
1025
1105
|
const cwd = process.cwd();
|
|
1026
1106
|
p2.intro(pc3.cyan("xera init --update"));
|
|
1027
|
-
const pkgPath =
|
|
1028
|
-
if (!
|
|
1107
|
+
const pkgPath = join9(cwd, "package.json");
|
|
1108
|
+
if (!existsSync9(pkgPath)) {
|
|
1029
1109
|
p2.cancel("No package.json found \u2014 run `xera init` first.");
|
|
1030
1110
|
process.exit(1);
|
|
1031
1111
|
}
|
|
@@ -1049,13 +1129,16 @@ async function initUpdateCommand(opts) {
|
|
|
1049
1129
|
pkg.scripts["xera:impact-prepare"] = "xera-internal impact-prepare";
|
|
1050
1130
|
pkg.scripts["xera:heal-prepare"] = "xera-internal heal-prepare";
|
|
1051
1131
|
pkg.scripts["xera:disputes"] = "xera-internal disputes";
|
|
1132
|
+
if (pkg.dependencies["@xera-ai/http"]) {
|
|
1133
|
+
pkg.scripts["xera:openapi-resolve"] = "xera-internal openapi-resolve";
|
|
1134
|
+
}
|
|
1052
1135
|
writeFileSync6(pkgPath, JSON.stringify(pkg, null, 2));
|
|
1053
|
-
const wfDir =
|
|
1054
|
-
|
|
1136
|
+
const wfDir = join9(cwd, ".github/workflows");
|
|
1137
|
+
mkdirSync6(wfDir, { recursive: true });
|
|
1055
1138
|
try {
|
|
1056
1139
|
const cliPkgPath = require3.resolve("@xera-ai/cli/package.json");
|
|
1057
|
-
const cliTplPath =
|
|
1058
|
-
|
|
1140
|
+
const cliTplPath = join9(cliPkgPath, "..", "templates/xera-graph.yml.template");
|
|
1141
|
+
copyFileSync2(cliTplPath, join9(wfDir, "xera-graph.yml"));
|
|
1059
1142
|
p2.log.info("scaffolded .github/workflows/xera-graph.yml");
|
|
1060
1143
|
} catch (_e) {
|
|
1061
1144
|
p2.log.warn("skipped xera-graph.yml scaffold (re-run `xera init` to create it)");
|
|
@@ -1070,14 +1153,14 @@ async function initUpdateCommand(opts) {
|
|
|
1070
1153
|
p2.log.warn("No editor integration detected in this project. Pass --editor claude|cursor|codex|all to add one.");
|
|
1071
1154
|
} else {
|
|
1072
1155
|
const skillsSrc = require3.resolve("@xera-ai/skills/package.json");
|
|
1073
|
-
const newSkillsDir =
|
|
1156
|
+
const newSkillsDir = join9(skillsSrc, "..");
|
|
1074
1157
|
const SKILL_IGNORE = new Set(["package.json", "version.json", "CHANGELOG.md"]);
|
|
1075
|
-
for (const name of
|
|
1158
|
+
for (const name of readdirSync4(newSkillsDir)) {
|
|
1076
1159
|
if (SKILL_IGNORE.has(name))
|
|
1077
1160
|
continue;
|
|
1078
1161
|
if (!name.endsWith(".md"))
|
|
1079
1162
|
continue;
|
|
1080
|
-
const rawNew = readFileSync5(
|
|
1163
|
+
const rawNew = readFileSync5(join9(newSkillsDir, name), "utf8");
|
|
1081
1164
|
const { frontmatter, body } = parseFrontmatter(rawNew);
|
|
1082
1165
|
const base = name.replace(/\.md$/, "");
|
|
1083
1166
|
const skillInput = { base, body, frontmatter };
|
|
@@ -1140,12 +1223,67 @@ async function initUpdateCommand(opts) {
|
|
|
1140
1223
|
p2.outro(pc3.green("Update complete. Run `xera doctor` to verify."));
|
|
1141
1224
|
}
|
|
1142
1225
|
|
|
1226
|
+
// src/commands/samples.ts
|
|
1227
|
+
import * as p3 from "@clack/prompts";
|
|
1228
|
+
import pc4 from "picocolors";
|
|
1229
|
+
async function samplesRemoveCommand(opts) {
|
|
1230
|
+
const cwd = process.cwd();
|
|
1231
|
+
p3.intro(pc4.cyan("xera samples remove"));
|
|
1232
|
+
const installed = detectInstalledSamples(cwd);
|
|
1233
|
+
if (installed.length === 0) {
|
|
1234
|
+
p3.log.info("No sample tickets found in .xera/");
|
|
1235
|
+
p3.outro(pc4.dim("nothing to do"));
|
|
1236
|
+
return 0;
|
|
1237
|
+
}
|
|
1238
|
+
let toRemove;
|
|
1239
|
+
if (opts.yes) {
|
|
1240
|
+
toRemove = installed;
|
|
1241
|
+
} else if (installed.length === 1) {
|
|
1242
|
+
const ok = await p3.confirm({
|
|
1243
|
+
message: `Remove .xera/${installed[0].id}/?`,
|
|
1244
|
+
initialValue: true
|
|
1245
|
+
});
|
|
1246
|
+
if (typeof ok === "symbol" || !ok) {
|
|
1247
|
+
p3.cancel("Aborted.");
|
|
1248
|
+
return 0;
|
|
1249
|
+
}
|
|
1250
|
+
toRemove = installed;
|
|
1251
|
+
} else {
|
|
1252
|
+
const all = allSamples();
|
|
1253
|
+
const choice = await p3.multiselect({
|
|
1254
|
+
message: "Which sample(s) to remove?",
|
|
1255
|
+
options: installed.map((s) => ({
|
|
1256
|
+
value: s.id,
|
|
1257
|
+
label: `.xera/${s.id}/`
|
|
1258
|
+
})),
|
|
1259
|
+
initialValues: all.map((s) => s.id),
|
|
1260
|
+
required: true
|
|
1261
|
+
});
|
|
1262
|
+
if (typeof choice === "symbol") {
|
|
1263
|
+
p3.cancel("Aborted.");
|
|
1264
|
+
return 0;
|
|
1265
|
+
}
|
|
1266
|
+
const set = new Set(choice);
|
|
1267
|
+
toRemove = installed.filter((s) => set.has(s.id));
|
|
1268
|
+
}
|
|
1269
|
+
let removed = 0;
|
|
1270
|
+
for (const s of toRemove) {
|
|
1271
|
+
const ok = removeSample(cwd, s);
|
|
1272
|
+
if (ok) {
|
|
1273
|
+
p3.log.success(`removed .xera/${s.id}/`);
|
|
1274
|
+
removed++;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
p3.outro(pc4.green(`removed ${removed} sample(s)`));
|
|
1278
|
+
return 0;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1143
1281
|
// src/index.ts
|
|
1144
1282
|
var require4 = createRequire3(import.meta.url);
|
|
1145
1283
|
var VERSION = require4("../package.json").version;
|
|
1146
1284
|
var VALID_SHAPES = ["web", "api", "mixed"];
|
|
1147
1285
|
var VALID_AUTH_STRATEGIES = ["bearer", "apiKey", "basic", "oauth-cc", "none"];
|
|
1148
|
-
var KNOWN_COMMANDS = ["init", "doctor"];
|
|
1286
|
+
var KNOWN_COMMANDS = ["init", "doctor", "samples"];
|
|
1149
1287
|
function levenshtein(a, b) {
|
|
1150
1288
|
const m = a.length;
|
|
1151
1289
|
const n = b.length;
|
|
@@ -1170,15 +1308,15 @@ function didYouMean(input) {
|
|
|
1170
1308
|
return bestDist <= 3 ? best : undefined;
|
|
1171
1309
|
}
|
|
1172
1310
|
function unknownCommand(input) {
|
|
1173
|
-
console.error(
|
|
1311
|
+
console.error(pc5.red(`
|
|
1174
1312
|
error: Unknown command '${input}'
|
|
1175
1313
|
`));
|
|
1176
1314
|
const suggestion = didYouMean(input);
|
|
1177
1315
|
if (suggestion) {
|
|
1178
|
-
console.error(` Did you mean ${
|
|
1316
|
+
console.error(` Did you mean ${pc5.cyan(suggestion)}?
|
|
1179
1317
|
`);
|
|
1180
1318
|
}
|
|
1181
|
-
console.error(` Run ${
|
|
1319
|
+
console.error(` Run ${pc5.cyan("xera --help")} to see available commands.
|
|
1182
1320
|
`);
|
|
1183
1321
|
process.exit(1);
|
|
1184
1322
|
}
|
|
@@ -1187,12 +1325,12 @@ async function main() {
|
|
|
1187
1325
|
cli.help();
|
|
1188
1326
|
cli.version(VERSION);
|
|
1189
1327
|
cli.usage("<command> [options]");
|
|
1190
|
-
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("--editor <list>", 'Editor(s) to scaffold: claude,cursor,codex or "all" (default: auto-detect or all)').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 web --editor claude,cursor").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) => {
|
|
1328
|
+
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("--editor <list>", 'Editor(s) to scaffold: claude,cursor,codex or "all" (default: auto-detect or all)').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)").option("--samples", "Scaffold sample ticket(s) under .xera/SAMPLE-001/ (web) or .xera/SAMPLE-HTTP-001/ (api) so /xera-run works out of the box").example("xera init").example("xera init -y --shape web").example("xera init -y --shape web --editor claude,cursor").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) => {
|
|
1191
1329
|
if (opts.update) {
|
|
1192
1330
|
const updateOpts = { yes: !!opts.yes };
|
|
1193
1331
|
if (opts.shape !== undefined) {
|
|
1194
1332
|
if (!VALID_SHAPES.includes(opts.shape)) {
|
|
1195
|
-
console.error(
|
|
1333
|
+
console.error(pc5.red(`
|
|
1196
1334
|
error: --shape must be one of: ${VALID_SHAPES.join(", ")}
|
|
1197
1335
|
`));
|
|
1198
1336
|
process.exit(1);
|
|
@@ -1201,7 +1339,7 @@ async function main() {
|
|
|
1201
1339
|
}
|
|
1202
1340
|
if (opts.authStrategy !== undefined) {
|
|
1203
1341
|
if (!VALID_AUTH_STRATEGIES.includes(opts.authStrategy)) {
|
|
1204
|
-
console.error(
|
|
1342
|
+
console.error(pc5.red(`
|
|
1205
1343
|
error: --auth-strategy must be one of: ${VALID_AUTH_STRATEGIES.join(", ")}
|
|
1206
1344
|
`));
|
|
1207
1345
|
process.exit(1);
|
|
@@ -1228,7 +1366,7 @@ async function main() {
|
|
|
1228
1366
|
const initOpts = { yes: !!opts.yes };
|
|
1229
1367
|
if (opts.shape !== undefined) {
|
|
1230
1368
|
if (!VALID_SHAPES.includes(opts.shape)) {
|
|
1231
|
-
console.error(
|
|
1369
|
+
console.error(pc5.red(`
|
|
1232
1370
|
error: --shape must be one of: ${VALID_SHAPES.join(", ")}
|
|
1233
1371
|
`));
|
|
1234
1372
|
process.exit(1);
|
|
@@ -1237,7 +1375,7 @@ async function main() {
|
|
|
1237
1375
|
}
|
|
1238
1376
|
if (opts.authStrategy !== undefined) {
|
|
1239
1377
|
if (!VALID_AUTH_STRATEGIES.includes(opts.authStrategy)) {
|
|
1240
|
-
console.error(
|
|
1378
|
+
console.error(pc5.red(`
|
|
1241
1379
|
error: --auth-strategy must be one of: ${VALID_AUTH_STRATEGIES.join(", ")}
|
|
1242
1380
|
`));
|
|
1243
1381
|
process.exit(1);
|
|
@@ -1266,8 +1404,14 @@ async function main() {
|
|
|
1266
1404
|
initOpts.httpRoles = opts.httpRoles;
|
|
1267
1405
|
if (opts.editor !== undefined)
|
|
1268
1406
|
initOpts.editor = opts.editor;
|
|
1407
|
+
if (opts.samples !== undefined)
|
|
1408
|
+
initOpts.samples = opts.samples;
|
|
1269
1409
|
await initCommand(initOpts);
|
|
1270
1410
|
});
|
|
1411
|
+
cli.command("samples remove", "Remove scaffolded sample tickets from .xera/").option("-y, --yes", "Skip confirmation; remove all installed samples").action(async (opts) => {
|
|
1412
|
+
const exit = await samplesRemoveCommand({ yes: !!opts.yes });
|
|
1413
|
+
process.exit(exit);
|
|
1414
|
+
});
|
|
1271
1415
|
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) => {
|
|
1272
1416
|
const exit = await doctorCommand(opts);
|
|
1273
1417
|
process.exit(exit);
|
|
@@ -1285,7 +1429,7 @@ async function main() {
|
|
|
1285
1429
|
cli.parse(process.argv, { run: false });
|
|
1286
1430
|
await cli.runMatchedCommand();
|
|
1287
1431
|
} catch (e) {
|
|
1288
|
-
console.error(
|
|
1432
|
+
console.error(pc5.red(`
|
|
1289
1433
|
error: ${e.message}
|
|
1290
1434
|
`));
|
|
1291
1435
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xera-ai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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.15.0",
|
|
19
|
+
"@xera-ai/skills": "^0.15.0",
|
|
20
20
|
"@clack/prompts": "1.4.0",
|
|
21
21
|
"cac": "7.0.0",
|
|
22
22
|
"picocolors": "1.1.1",
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# SAMPLE-HTTP-001 — Create user via API
|
|
2
|
+
|
|
3
|
+
## Background
|
|
4
|
+
|
|
5
|
+
A first-touch sample ticket for HTTP API testing scaffolded by
|
|
6
|
+
`xera init --samples`. Use it to learn the API pipeline end-to-end without a
|
|
7
|
+
Jira ticket. If `http.spec` is configured, `/xera-script` will use the
|
|
8
|
+
OpenAPI schema to inform body shape + assertions.
|
|
9
|
+
|
|
10
|
+
## User story
|
|
11
|
+
|
|
12
|
+
As an API consumer with a bearer token
|
|
13
|
+
I want to POST /users with a JSON body
|
|
14
|
+
So that a new user is created with a stable id.
|
|
15
|
+
|
|
16
|
+
## Acceptance criteria
|
|
17
|
+
|
|
18
|
+
- Given I have a valid bearer token for the `user` role
|
|
19
|
+
- When I POST `/users` with `{ name: "Alice", email: "alice@example.com" }`
|
|
20
|
+
- Then the response status is `201`
|
|
21
|
+
- And the response body has a non-empty `id` string
|
|
22
|
+
- And the response body's `email` equals the email I sent
|
|
23
|
+
|
|
24
|
+
- Given I have a valid bearer token
|
|
25
|
+
- When I POST `/users` with `{ email: "missing-name@example.com" }` (no name)
|
|
26
|
+
- Then the response status is `422`
|
|
27
|
+
- And the response body has an `errors` array
|
|
28
|
+
|
|
29
|
+
- Given I have an expired or invalid token
|
|
30
|
+
- When I POST `/users` with any body
|
|
31
|
+
- Then the response status is `401`
|
|
32
|
+
|
|
33
|
+
## Notes
|
|
34
|
+
|
|
35
|
+
- `meta.json.source` is `"local"`, so `/xera-run SAMPLE-HTTP-001` will NOT post to Jira.
|
|
36
|
+
- Use `process.env.XERA_RUN_ID` in emails to keep test runs isolated, e.g.
|
|
37
|
+
`email: \`alice-${process.env.XERA_RUN_ID}@example.com\``.
|
|
38
|
+
- Remove the sample later with `xera samples remove`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Feature: SAMPLE-HTTP-001 — Create user via API
|
|
2
|
+
|
|
3
|
+
Background:
|
|
4
|
+
Given I am authenticated as the "user" role
|
|
5
|
+
|
|
6
|
+
Scenario: Valid payload creates a user
|
|
7
|
+
When I POST "/users" with a name and email
|
|
8
|
+
Then the response status is 201
|
|
9
|
+
And the response body has a non-empty "id"
|
|
10
|
+
And the response body "email" equals the sent email
|
|
11
|
+
|
|
12
|
+
Scenario: Missing name returns 422 with errors
|
|
13
|
+
When I POST "/users" with only an email
|
|
14
|
+
Then the response status is 422
|
|
15
|
+
And the response body has an "errors" array
|
|
16
|
+
|
|
17
|
+
Scenario: Expired token returns 401
|
|
18
|
+
Given my token is expired
|
|
19
|
+
When I POST "/users" with a name and email
|
|
20
|
+
Then the response status is 401
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# SAMPLE-001 — User signs in to the dashboard
|
|
2
|
+
|
|
3
|
+
## Background
|
|
4
|
+
|
|
5
|
+
A first-touch sample ticket scaffolded by `xera init --samples`. Use it to learn
|
|
6
|
+
the pipeline end-to-end without needing a Jira ticket.
|
|
7
|
+
|
|
8
|
+
## User story
|
|
9
|
+
|
|
10
|
+
As a registered user
|
|
11
|
+
I want to sign in with my email and password
|
|
12
|
+
So that I can access my dashboard.
|
|
13
|
+
|
|
14
|
+
## Acceptance criteria
|
|
15
|
+
|
|
16
|
+
- Given I am on the login page
|
|
17
|
+
- When I enter a valid email and password and click "Sign in"
|
|
18
|
+
- Then I am redirected to `/dashboard`
|
|
19
|
+
- And I see my name in the top-right header
|
|
20
|
+
|
|
21
|
+
- Given I am on the login page
|
|
22
|
+
- When I enter an incorrect password
|
|
23
|
+
- Then I see the error "Invalid email or password"
|
|
24
|
+
- And I remain on the login page
|
|
25
|
+
|
|
26
|
+
## Notes
|
|
27
|
+
|
|
28
|
+
- `meta.json.source` is `"local"`, so `/xera-run SAMPLE-001` will NOT post to Jira.
|
|
29
|
+
- Remove the sample later with `xera samples remove`.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Feature: SAMPLE-001 — Sign in flow
|
|
2
|
+
|
|
3
|
+
Background:
|
|
4
|
+
Given I am on the login page
|
|
5
|
+
|
|
6
|
+
Scenario: Valid credentials redirect to dashboard
|
|
7
|
+
When I enter a valid email and password
|
|
8
|
+
And I click "Sign in"
|
|
9
|
+
Then I am redirected to "/dashboard"
|
|
10
|
+
And I see my display name in the header
|
|
11
|
+
|
|
12
|
+
Scenario: Invalid password shows an inline error
|
|
13
|
+
When I enter a valid email and an invalid password
|
|
14
|
+
And I click "Sign in"
|
|
15
|
+
Then I see the error "Invalid email or password"
|
|
16
|
+
And I remain on the login page
|