@xera-ai/cli 0.12.1 → 0.12.3

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.
Files changed (2) hide show
  1. package/dist/index.js +185 -55
  2. package/package.json +5 -4
package/dist/index.js CHANGED
@@ -15,7 +15,95 @@ import pc from "picocolors";
15
15
  import { existsSync, readFileSync } from "fs";
16
16
  import { join } from "path";
17
17
  import { loadConfig, readAuthState } from "@xera-ai/core";
18
- async function runChecks(cwd) {
18
+ import { parse as parseYaml } from "yaml";
19
+ function pushTicketChecks(checks, cwd, ticket, acFieldConfigured) {
20
+ const ticketDir = join(cwd, ".xera", ticket);
21
+ if (!existsSync(ticketDir)) {
22
+ checks.push({
23
+ name: `${ticket}: .xera/${ticket}/ exists`,
24
+ ok: false,
25
+ message: `no artifact dir \u2014 run \`/xera-fetch ${ticket}\` first`
26
+ });
27
+ return;
28
+ }
29
+ const giPath = join(ticketDir, "graph-input.json");
30
+ if (!existsSync(giPath)) {
31
+ checks.push({
32
+ name: `${ticket}: graph-input.json present`,
33
+ ok: false,
34
+ message: `missing \u2014 modifiesAreas will be []; run step 5 of /xera-fetch (extract-areas prompt)`
35
+ });
36
+ } else {
37
+ try {
38
+ const data = JSON.parse(readFileSync(giPath, "utf8"));
39
+ if (!Array.isArray(data.modifiesAreas)) {
40
+ checks.push({
41
+ name: `${ticket}: graph-input.json present`,
42
+ ok: false,
43
+ message: `parsed but modifiesAreas is not an array \u2014 re-run step 5 of /xera-fetch`
44
+ });
45
+ } else {
46
+ checks.push({
47
+ name: `${ticket}: graph-input.json present`,
48
+ ok: true,
49
+ message: `${data.modifiesAreas.length} area(s)`
50
+ });
51
+ }
52
+ } catch (e) {
53
+ checks.push({
54
+ name: `${ticket}: graph-input.json present`,
55
+ ok: false,
56
+ message: `invalid JSON (${e.message}) \u2014 re-run step 5 of /xera-fetch`
57
+ });
58
+ }
59
+ }
60
+ const storyPath = join(ticketDir, "story.md");
61
+ if (!existsSync(storyPath)) {
62
+ checks.push({
63
+ name: `${ticket}: story.md acceptanceCriteria`,
64
+ ok: false,
65
+ message: `story.md missing \u2014 re-run /xera-fetch ${ticket}`
66
+ });
67
+ return;
68
+ }
69
+ const raw = readFileSync(storyPath, "utf8");
70
+ const m = raw.match(/^---\n([\s\S]*?)\n---/);
71
+ if (!m) {
72
+ checks.push({
73
+ name: `${ticket}: story.md acceptanceCriteria`,
74
+ ok: false,
75
+ message: `frontmatter missing \u2014 re-run /xera-fetch ${ticket}`
76
+ });
77
+ return;
78
+ }
79
+ let fm;
80
+ try {
81
+ fm = parseYaml(m[1]);
82
+ } catch (e) {
83
+ checks.push({
84
+ name: `${ticket}: story.md acceptanceCriteria`,
85
+ ok: false,
86
+ message: `frontmatter unparseable (${e.message})`
87
+ });
88
+ return;
89
+ }
90
+ const ac = Array.isArray(fm.acceptanceCriteria) ? fm.acceptanceCriteria : [];
91
+ if (ac.length === 0) {
92
+ const hint = acFieldConfigured ? `jira.fields.acceptanceCriteria is configured but Jira returned no AC for this ticket \u2014 check the ticket in Jira` : `no AC in frontmatter; AC-level coverage will be empty. Set jira.fields.acceptanceCriteria in xera.config.ts if your project stores AC in a dedicated Jira field`;
93
+ checks.push({
94
+ name: `${ticket}: story.md acceptanceCriteria`,
95
+ ok: false,
96
+ message: hint
97
+ });
98
+ } else {
99
+ checks.push({
100
+ name: `${ticket}: story.md acceptanceCriteria`,
101
+ ok: true,
102
+ message: `${ac.length} AC item(s)`
103
+ });
104
+ }
105
+ }
106
+ async function runChecks(cwd, opts = {}) {
19
107
  const checks = [];
20
108
  checks.push({
21
109
  name: `bun ${process.versions.bun ?? "unknown"}`,
@@ -181,6 +269,10 @@ async function runChecks(cwd) {
181
269
  }
182
270
  } catch {}
183
271
  }
272
+ if (opts.ticket) {
273
+ const acFieldConfigured = Boolean(cfg.jira?.fields?.acceptanceCriteria);
274
+ pushTicketChecks(checks, cwd, opts.ticket, acFieldConfigured);
275
+ }
184
276
  } catch (e) {
185
277
  checks.push({
186
278
  name: "xera.config.ts found and valid",
@@ -210,21 +302,34 @@ async function runChecks(cwd) {
210
302
  checks.push({ name: "xera skills present", ok: false, message: "run `xera init`" });
211
303
  } else {
212
304
  const required = [
213
- "xera-run.md",
214
- "xera-fetch.md",
215
- "xera-feature.md",
216
- "xera-script.md",
217
- "xera-exec.md",
218
- "xera-report.md",
219
- "xera-promote.md"
305
+ "xera-run",
306
+ "xera-fetch",
307
+ "xera-feature",
308
+ "xera-script",
309
+ "xera-exec",
310
+ "xera-report",
311
+ "xera-promote"
220
312
  ];
221
- const missing = required.filter((n) => !existsSync(join(skillsDir, n)));
313
+ const missing = [];
314
+ const legacyFlat = [];
315
+ for (const base of required) {
316
+ if (existsSync(join(skillsDir, base, "SKILL.md")))
317
+ continue;
318
+ if (existsSync(join(skillsDir, `${base}.md`))) {
319
+ legacyFlat.push(base);
320
+ } else {
321
+ missing.push(base);
322
+ }
323
+ }
222
324
  const skillsCheck = {
223
325
  name: "xera skills present",
224
- ok: missing.length === 0
326
+ ok: missing.length === 0 && legacyFlat.length === 0
225
327
  };
226
- if (missing.length)
227
- skillsCheck.message = `missing: ${missing.join(", ")}`;
328
+ if (missing.length) {
329
+ skillsCheck.message = `missing: ${missing.map((b) => `${b}/SKILL.md`).join(", ")}`;
330
+ } else if (legacyFlat.length) {
331
+ skillsCheck.message = `legacy flat layout in .claude/skills/ \u2014 run \`xera init --update\` to migrate to <name>/SKILL.md (Claude Code Skill tool requires the directory layout)`;
332
+ }
228
333
  checks.push(skillsCheck);
229
334
  }
230
335
  return checks;
@@ -248,7 +353,7 @@ async function doctorCommand(opts) {
248
353
  console.log("Token usage estimation requires log lines with tokens_in/tokens_out fields (added by skills).");
249
354
  return 0;
250
355
  }
251
- const checks = await runChecks(cwd);
356
+ const checks = await runChecks(cwd, opts.strict ? { ticket: opts.strict } : {});
252
357
  for (const c of checks) {
253
358
  const icon = c.ok ? pc.green("\u2713") : pc.red("\u2717");
254
359
  console.log(`${icon} ${c.name}${c.message ? pc.dim(` \u2014 ${c.message}`) : ""}`);
@@ -261,9 +366,16 @@ async function doctorCommand(opts) {
261
366
  }
262
367
 
263
368
  // src/commands/init.ts
264
- import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
369
+ import {
370
+ appendFileSync,
371
+ existsSync as existsSync3,
372
+ mkdirSync as mkdirSync2,
373
+ readdirSync as readdirSync2,
374
+ readFileSync as readFileSync3,
375
+ writeFileSync as writeFileSync2
376
+ } from "fs";
265
377
  import { createRequire } from "module";
266
- import { join as join3 } from "path";
378
+ import { dirname as dirname2, join as join3 } from "path";
267
379
  import * as p from "@clack/prompts";
268
380
  import { generateKey } from "@xera-ai/core";
269
381
  import pc2 from "picocolors";
@@ -293,17 +405,6 @@ function scaffoldFile(targetPath, templateName, vars) {
293
405
  mkdirSync(dirname(targetPath), { recursive: true });
294
406
  writeFileSync(targetPath, render(tmpl, vars));
295
407
  }
296
- function copyDir(src, dest) {
297
- mkdirSync(dest, { recursive: true });
298
- for (const entry of readdirSync(src, { withFileTypes: true })) {
299
- const s = join2(src, entry.name);
300
- const d = join2(dest, entry.name);
301
- if (entry.isDirectory())
302
- copyDir(s, d);
303
- else
304
- writeFileSync(d, readFileSync2(s));
305
- }
306
- }
307
408
 
308
409
  // src/commands/init.ts
309
410
  var require2 = createRequire(import.meta.url);
@@ -446,15 +547,22 @@ async function initCommand(opts) {
446
547
  writeFileSync2(gitignorePath, `${gitignoreAdditions.trim()}
447
548
  `);
448
549
  }
449
- const skillsSrc = require2.resolve("@xera-ai/skills/package.json");
450
- const skillsDir = join3(skillsSrc, "..");
451
- for (const target of [".claude/skills", ".claude/commands"]) {
452
- copyDir(skillsDir, join3(cwd, target));
453
- for (const name of ["package.json", "version.json", "CHANGELOG.md"]) {
454
- const f = join3(cwd, target, name);
455
- if (existsSync3(f))
456
- unlinkSync(f);
457
- }
550
+ const skillsPkgPath = require2.resolve("@xera-ai/skills/package.json");
551
+ const skillsSrcDir = join3(skillsPkgPath, "..");
552
+ const SKILL_IGNORE = new Set(["package.json", "version.json", "CHANGELOG.md"]);
553
+ for (const name of readdirSync2(skillsSrcDir)) {
554
+ if (SKILL_IGNORE.has(name))
555
+ continue;
556
+ if (!name.endsWith(".md"))
557
+ continue;
558
+ const content = readFileSync3(join3(skillsSrcDir, name));
559
+ const base = name.replace(/\.md$/, "");
560
+ const skillFile = join3(cwd, ".claude/skills", base, "SKILL.md");
561
+ mkdirSync2(dirname2(skillFile), { recursive: true });
562
+ writeFileSync2(skillFile, content);
563
+ const cmdFile = join3(cwd, ".claude/commands", name);
564
+ mkdirSync2(dirname2(cmdFile), { recursive: true });
565
+ writeFileSync2(cmdFile, content);
458
566
  }
459
567
  const pkgPath = join3(cwd, "package.json");
460
568
  const pkg = existsSync3(pkgPath) ? JSON.parse(readFileSync3(pkgPath, "utf8")) : { name: "xera-project", private: true, type: "module" };
@@ -528,13 +636,14 @@ Next:
528
636
  import {
529
637
  copyFileSync,
530
638
  existsSync as existsSync4,
531
- mkdirSync as mkdirSync2,
532
- readdirSync as readdirSync2,
639
+ mkdirSync as mkdirSync3,
640
+ readdirSync as readdirSync3,
533
641
  readFileSync as readFileSync4,
642
+ unlinkSync,
534
643
  writeFileSync as writeFileSync3
535
644
  } from "fs";
536
645
  import { createRequire as createRequire2 } from "module";
537
- import { dirname as dirname2, join as join4 } from "path";
646
+ import { dirname as dirname3, join as join4 } from "path";
538
647
  import * as p2 from "@clack/prompts";
539
648
  import pc3 from "picocolors";
540
649
  var require3 = createRequire2(import.meta.url);
@@ -647,7 +756,7 @@ async function initUpdateCommand(opts) {
647
756
  pkg.scripts["xera:disputes"] = "xera-internal disputes";
648
757
  writeFileSync3(pkgPath, JSON.stringify(pkg, null, 2));
649
758
  const wfDir = join4(cwd, ".github/workflows");
650
- mkdirSync2(wfDir, { recursive: true });
759
+ mkdirSync3(wfDir, { recursive: true });
651
760
  try {
652
761
  const cliPkgPath = require3.resolve("@xera-ai/cli/package.json");
653
762
  const cliTplPath = join4(cliPkgPath, "..", "templates/xera-graph.yml.template");
@@ -658,28 +767,47 @@ async function initUpdateCommand(opts) {
658
767
  }
659
768
  const skillsSrc = require3.resolve("@xera-ai/skills/package.json");
660
769
  const newSkillsDir = join4(skillsSrc, "..");
661
- const targetDirs = [join4(cwd, ".claude/skills"), join4(cwd, ".claude/commands")];
662
- for (const name of readdirSync2(newSkillsDir)) {
770
+ const SKILL_IGNORE = new Set(["package.json", "version.json", "CHANGELOG.md"]);
771
+ for (const name of readdirSync3(newSkillsDir)) {
772
+ if (SKILL_IGNORE.has(name))
773
+ continue;
663
774
  if (!name.endsWith(".md"))
664
775
  continue;
665
776
  const newContent = readFileSync4(join4(newSkillsDir, name), "utf8");
666
- const localStates = targetDirs.map((dir) => {
667
- const path = join4(dir, name);
668
- if (!existsSync4(path))
669
- return { path, state: "missing" };
670
- const content = readFileSync4(path, "utf8");
671
- return { path, state: content === newContent ? "same" : "diff" };
672
- });
673
- if (localStates.every((s) => s.state === "missing")) {
674
- for (const { path } of localStates) {
675
- mkdirSync2(dirname2(path), { recursive: true });
777
+ const base = name.replace(/\.md$/, "");
778
+ const skillPath = join4(cwd, ".claude/skills", base, "SKILL.md");
779
+ const legacyFlatSkillPath = join4(cwd, ".claude/skills", name);
780
+ const cmdPath = join4(cwd, ".claude/commands", name);
781
+ let migratedLegacy = false;
782
+ if (existsSync4(legacyFlatSkillPath) && !existsSync4(skillPath)) {
783
+ const legacyContent = readFileSync4(legacyFlatSkillPath, "utf8");
784
+ mkdirSync3(dirname3(skillPath), { recursive: true });
785
+ writeFileSync3(skillPath, legacyContent);
786
+ unlinkSync(legacyFlatSkillPath);
787
+ migratedLegacy = true;
788
+ }
789
+ const targets = [];
790
+ for (const path of [skillPath, cmdPath]) {
791
+ if (!existsSync4(path)) {
792
+ targets.push({ path, state: "missing" });
793
+ } else {
794
+ const content = readFileSync4(path, "utf8");
795
+ targets.push({ path, state: content === newContent ? "same" : "diff" });
796
+ }
797
+ }
798
+ if (targets.every((s) => s.state === "missing")) {
799
+ for (const { path } of targets) {
800
+ mkdirSync3(dirname3(path), { recursive: true });
676
801
  writeFileSync3(path, newContent);
677
802
  }
678
803
  p2.log.info(`+ ${name}`);
679
804
  continue;
680
805
  }
681
- if (localStates.every((s) => s.state === "same")) {
682
- p2.log.info(`= ${name}`);
806
+ if (targets.every((s) => s.state === "same")) {
807
+ if (migratedLegacy)
808
+ p2.log.success(`migrated ${name} to .claude/skills/${base}/SKILL.md`);
809
+ else
810
+ p2.log.info(`= ${name}`);
683
811
  continue;
684
812
  }
685
813
  const choice = await p2.select({
@@ -690,12 +818,14 @@ async function initUpdateCommand(opts) {
690
818
  ]
691
819
  });
692
820
  if (choice === "overwrite") {
693
- for (const { path } of localStates) {
694
- mkdirSync2(dirname2(path), { recursive: true });
821
+ for (const { path } of targets) {
822
+ mkdirSync3(dirname3(path), { recursive: true });
695
823
  writeFileSync3(path, newContent);
696
824
  }
697
825
  p2.log.success(`overwrote ${name}`);
698
826
  } else {
827
+ if (migratedLegacy)
828
+ p2.log.success(`migrated ${name} to .claude/skills/${base}/SKILL.md`);
699
829
  p2.log.warn(`kept local ${name}`);
700
830
  }
701
831
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xera-ai/cli",
3
- "version": "0.12.1",
3
+ "version": "0.12.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "xera": "./bin/xera"
@@ -15,10 +15,11 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "@xera-ai/core": "^0.12.1",
19
- "@xera-ai/skills": "^0.12.1",
18
+ "@xera-ai/core": "^0.12.3",
19
+ "@xera-ai/skills": "^0.12.3",
20
20
  "@clack/prompts": "1.4.0",
21
21
  "cac": "7.0.0",
22
- "picocolors": "1.1.1"
22
+ "picocolors": "1.1.1",
23
+ "yaml": "2.9.0"
23
24
  }
24
25
  }