@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.
- package/dist/index.js +185 -55
- 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
|
-
|
|
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
|
|
214
|
-
"xera-fetch
|
|
215
|
-
"xera-feature
|
|
216
|
-
"xera-script
|
|
217
|
-
"xera-exec
|
|
218
|
-
"xera-report
|
|
219
|
-
"xera-promote
|
|
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 =
|
|
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 {
|
|
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
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
|
532
|
-
readdirSync as
|
|
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
|
|
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
|
-
|
|
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
|
|
662
|
-
for (const name of
|
|
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
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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 (
|
|
682
|
-
|
|
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
|
|
694
|
-
|
|
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.
|
|
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.
|
|
19
|
-
"@xera-ai/skills": "^0.12.
|
|
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
|
}
|