@xera-ai/cli 0.13.1 → 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 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 pc4 from "picocolors";
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 existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
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 join7 } from "path";
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(join7(cwd, "xera.config.ts"), configTmpl, vars);
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(join7(cwd, "playwright.config.ts"), pwTmpl, vars);
778
- scaffoldFile(join7(cwd, "tsconfig.json"), "tsconfig.json.tmpl", vars);
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(join7(cwd, ".env.example"), "http-env.example.tmpl", vars);
840
+ scaffoldFile(join8(cwd, ".env.example"), "http-env.example.tmpl", vars);
781
841
  } else {
782
- scaffoldFile(join7(cwd, ".env.example"), "env.example.tmpl", vars);
842
+ scaffoldFile(join8(cwd, ".env.example"), "env.example.tmpl", vars);
783
843
  }
784
844
  if (wantsWeb || wantsHttp) {
785
- scaffoldFile(join7(cwd, "shared/auth-setup.ts"), "auth-setup.ts.tmpl", vars);
845
+ scaffoldFile(join8(cwd, "shared/auth-setup.ts"), "auth-setup.ts.tmpl", vars);
786
846
  }
787
- scaffoldFile(join7(cwd, ".github/workflows/xera-graph.yml"), "xera-graph.yml.template", vars);
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 = join7(cwd, vars.openapiPath);
790
- if (!existsSync7(openapiTarget)) {
849
+ const openapiTarget = join8(cwd, vars.openapiPath);
850
+ if (!existsSync8(openapiTarget)) {
791
851
  scaffoldFile(openapiTarget, "openapi.yaml.tmpl", vars);
792
852
  }
793
853
  }
794
- const gitignorePath = join7(cwd, ".gitignore");
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 (existsSync7(gitignorePath)) {
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 = join7(skillsPkgPath, "..");
908
+ const skillsSrcDir = join8(skillsPkgPath, "..");
837
909
  const SKILL_IGNORE = new Set(["package.json", "version.json", "CHANGELOG.md"]);
838
- for (const name of readdirSync2(skillsSrcDir)) {
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(join7(skillsSrcDir, name), "utf8");
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 = join7(cwd, "package.json");
854
- const pkg = existsSync7(pkgPath) ? JSON.parse(readFileSync4(pkgPath, "utf8")) : { name: "xera-project", private: true, type: "module" };
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
- p.note(nextSteps.trim(), "Next steps");
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 existsSync8,
935
- mkdirSync as mkdirSync5,
936
- readdirSync as readdirSync3,
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 join8 } from "path";
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 = join8(cwd, "xera.config.ts");
948
- if (!existsSync8(configPath))
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 = join8(cwd, "package.json");
1028
- if (!existsSync8(pkgPath)) {
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 = join8(cwd, ".github/workflows");
1054
- mkdirSync5(wfDir, { recursive: true });
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 = join8(cliPkgPath, "..", "templates/xera-graph.yml.template");
1058
- copyFileSync(cliTplPath, join8(wfDir, "xera-graph.yml"));
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 = join8(skillsSrc, "..");
1156
+ const newSkillsDir = join9(skillsSrc, "..");
1074
1157
  const SKILL_IGNORE = new Set(["package.json", "version.json", "CHANGELOG.md"]);
1075
- for (const name of readdirSync3(newSkillsDir)) {
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(join8(newSkillsDir, name), "utf8");
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(pc4.red(`
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 ${pc4.cyan(suggestion)}?
1316
+ console.error(` Did you mean ${pc5.cyan(suggestion)}?
1179
1317
  `);
1180
1318
  }
1181
- console.error(` Run ${pc4.cyan("xera --help")} to see available commands.
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(pc4.red(`
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(pc4.red(`
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(pc4.red(`
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(pc4.red(`
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(pc4.red(`
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.13.1",
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.13.1",
19
- "@xera-ai/skills": "^0.13.1",
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,7 @@
1
+ {
2
+ "ticket": "SAMPLE-HTTP-001",
3
+ "adapter": "http",
4
+ "source": "local",
5
+ "xera_version": "{{cliVersion}}",
6
+ "prompts_version": "{{cliVersion}}"
7
+ }
@@ -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,7 @@
1
+ {
2
+ "ticket": "SAMPLE-001",
3
+ "adapter": "web",
4
+ "source": "local",
5
+ "xera_version": "{{cliVersion}}",
6
+ "prompts_version": "{{cliVersion}}"
7
+ }
@@ -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