@xera-ai/cli 0.12.3 → 0.13.1
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 +454 -187
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -7,18 +7,265 @@ import { cac } from "cac";
|
|
|
7
7
|
import pc4 from "picocolors";
|
|
8
8
|
|
|
9
9
|
// src/commands/doctor.ts
|
|
10
|
-
import { existsSync as
|
|
10
|
+
import { existsSync as existsSync6 } from "fs";
|
|
11
11
|
import { NdjsonLogger, resolveArtifactPaths } from "@xera-ai/core";
|
|
12
12
|
import pc from "picocolors";
|
|
13
13
|
|
|
14
14
|
// src/checks.ts
|
|
15
|
-
import { existsSync, readFileSync } from "fs";
|
|
16
|
-
import { join } from "path";
|
|
15
|
+
import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
|
|
16
|
+
import { join as join5 } from "path";
|
|
17
17
|
import { loadConfig, readAuthState } from "@xera-ai/core";
|
|
18
18
|
import { parse as parseYaml } from "yaml";
|
|
19
|
+
|
|
20
|
+
// src/editors/claude.ts
|
|
21
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
22
|
+
import { dirname, join } from "path";
|
|
23
|
+
|
|
24
|
+
// src/editors/frontmatter.ts
|
|
25
|
+
var FENCE = "---";
|
|
26
|
+
function parseFrontmatter(md) {
|
|
27
|
+
if (!md.startsWith(`${FENCE}
|
|
28
|
+
`)) {
|
|
29
|
+
return { frontmatter: { raw: "", fields: {} }, body: md };
|
|
30
|
+
}
|
|
31
|
+
const closeIdx = md.indexOf(`
|
|
32
|
+
${FENCE}
|
|
33
|
+
`, FENCE.length + 1);
|
|
34
|
+
if (closeIdx < 0) {
|
|
35
|
+
return { frontmatter: { raw: "", fields: {} }, body: md };
|
|
36
|
+
}
|
|
37
|
+
const raw = md.slice(FENCE.length + 1, closeIdx);
|
|
38
|
+
const body = md.slice(closeIdx + `
|
|
39
|
+
${FENCE}
|
|
40
|
+
`.length);
|
|
41
|
+
const fields = {};
|
|
42
|
+
const lines = raw.split(`
|
|
43
|
+
`);
|
|
44
|
+
for (let i = 0;i < lines.length; i++) {
|
|
45
|
+
const line = lines[i];
|
|
46
|
+
if (!line.trim())
|
|
47
|
+
continue;
|
|
48
|
+
const m = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
49
|
+
if (!m)
|
|
50
|
+
continue;
|
|
51
|
+
const key = m[1];
|
|
52
|
+
const value = m[2];
|
|
53
|
+
if (value === "|") {
|
|
54
|
+
const collected = [];
|
|
55
|
+
while (i + 1 < lines.length) {
|
|
56
|
+
const next = lines[i + 1];
|
|
57
|
+
if (/^ {2,}/.test(next)) {
|
|
58
|
+
collected.push(next.replace(/^ {2}/, ""));
|
|
59
|
+
i++;
|
|
60
|
+
} else if (next.trim() === "") {
|
|
61
|
+
collected.push("");
|
|
62
|
+
i++;
|
|
63
|
+
} else {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
while (collected.length && collected[collected.length - 1] === "")
|
|
68
|
+
collected.pop();
|
|
69
|
+
fields[key] = collected.join(`
|
|
70
|
+
`);
|
|
71
|
+
} else if (value === "true" || value === "false") {
|
|
72
|
+
fields[key] = value === "true";
|
|
73
|
+
} else if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
74
|
+
fields[key] = value.slice(1, -1).replace(/\\"/g, '"');
|
|
75
|
+
} else {
|
|
76
|
+
fields[key] = value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { frontmatter: { raw, fields }, body };
|
|
80
|
+
}
|
|
81
|
+
function serializeFrontmatter(fields) {
|
|
82
|
+
const lines = [FENCE];
|
|
83
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
84
|
+
if (typeof value === "boolean") {
|
|
85
|
+
lines.push(`${key}: ${value}`);
|
|
86
|
+
} else if (Array.isArray(value)) {
|
|
87
|
+
lines.push(`${key}: [${value.join(", ")}]`);
|
|
88
|
+
} else if (value.includes(`
|
|
89
|
+
`)) {
|
|
90
|
+
lines.push(`${key}: |`);
|
|
91
|
+
for (const sub of value.split(`
|
|
92
|
+
`))
|
|
93
|
+
lines.push(` ${sub}`);
|
|
94
|
+
} else if (/[:#]/.test(value)) {
|
|
95
|
+
lines.push(`${key}: "${value.replace(/"/g, "\\\"")}"`);
|
|
96
|
+
} else {
|
|
97
|
+
lines.push(`${key}: ${value}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
lines.push(FENCE);
|
|
101
|
+
return `${lines.join(`
|
|
102
|
+
`)}
|
|
103
|
+
`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/editors/claude.ts
|
|
107
|
+
function renderSource(input) {
|
|
108
|
+
return serializeFrontmatter(input.frontmatter.fields) + input.body;
|
|
109
|
+
}
|
|
110
|
+
var claudeAdapter = {
|
|
111
|
+
name: "claude",
|
|
112
|
+
detect(cwd) {
|
|
113
|
+
return existsSync(join(cwd, ".claude"));
|
|
114
|
+
},
|
|
115
|
+
scaffoldSkill(cwd, input) {
|
|
116
|
+
const target = join(cwd, ".claude/skills", input.base, "SKILL.md");
|
|
117
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
118
|
+
writeFileSync(target, renderSource(input));
|
|
119
|
+
},
|
|
120
|
+
scaffoldCommand(cwd, input) {
|
|
121
|
+
const target = join(cwd, ".claude/commands", `${input.base}.md`);
|
|
122
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
123
|
+
writeFileSync(target, renderSource(input));
|
|
124
|
+
},
|
|
125
|
+
legacyMigrate(cwd, base) {
|
|
126
|
+
const flat = join(cwd, ".claude/skills", `${base}.md`);
|
|
127
|
+
const dir = join(cwd, ".claude/skills", base, "SKILL.md");
|
|
128
|
+
if (!existsSync(flat) || existsSync(dir))
|
|
129
|
+
return false;
|
|
130
|
+
const content = readFileSync(flat);
|
|
131
|
+
mkdirSync(dirname(dir), { recursive: true });
|
|
132
|
+
writeFileSync(dir, content);
|
|
133
|
+
unlinkSync(flat);
|
|
134
|
+
return true;
|
|
135
|
+
},
|
|
136
|
+
doctorChecks(cwd, requiredSkills) {
|
|
137
|
+
const skillsDir = join(cwd, ".claude/skills");
|
|
138
|
+
if (!existsSync(skillsDir)) {
|
|
139
|
+
return [{ name: "xera skills present (claude)", ok: false, message: "run `xera init`" }];
|
|
140
|
+
}
|
|
141
|
+
const missing = [];
|
|
142
|
+
const legacyFlat = [];
|
|
143
|
+
for (const base of requiredSkills) {
|
|
144
|
+
if (existsSync(join(skillsDir, base, "SKILL.md")))
|
|
145
|
+
continue;
|
|
146
|
+
if (existsSync(join(skillsDir, `${base}.md`)))
|
|
147
|
+
legacyFlat.push(base);
|
|
148
|
+
else
|
|
149
|
+
missing.push(base);
|
|
150
|
+
}
|
|
151
|
+
const check = {
|
|
152
|
+
name: "xera skills present (claude)",
|
|
153
|
+
ok: missing.length === 0 && legacyFlat.length === 0
|
|
154
|
+
};
|
|
155
|
+
if (missing.length) {
|
|
156
|
+
check.message = `missing: ${missing.map((b) => `${b}/SKILL.md`).join(", ")}`;
|
|
157
|
+
} else if (legacyFlat.length) {
|
|
158
|
+
check.message = `legacy flat layout \u2014 run \`xera init --update --editor claude\` to migrate`;
|
|
159
|
+
}
|
|
160
|
+
return [check];
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// src/editors/codex.ts
|
|
165
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
166
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
167
|
+
var codexAdapter = {
|
|
168
|
+
name: "codex",
|
|
169
|
+
detect(cwd) {
|
|
170
|
+
return existsSync2(join2(cwd, ".agents"));
|
|
171
|
+
},
|
|
172
|
+
scaffoldSkill(cwd, input) {
|
|
173
|
+
const target = join2(cwd, ".agents/skills", input.base, "SKILL.md");
|
|
174
|
+
mkdirSync2(dirname2(target), { recursive: true });
|
|
175
|
+
writeFileSync2(target, serializeFrontmatter(input.frontmatter.fields) + input.body);
|
|
176
|
+
},
|
|
177
|
+
doctorChecks(cwd, requiredSkills) {
|
|
178
|
+
const skillsDir = join2(cwd, ".agents/skills");
|
|
179
|
+
const missing = requiredSkills.filter((b) => !existsSync2(join2(skillsDir, b, "SKILL.md")));
|
|
180
|
+
const check = {
|
|
181
|
+
name: "xera skills present (codex)",
|
|
182
|
+
ok: missing.length === 0
|
|
183
|
+
};
|
|
184
|
+
if (missing.length) {
|
|
185
|
+
check.message = `missing: ${missing.map((b) => `${b}/SKILL.md`).join(", ")} \u2014 run \`xera init --update --editor codex\``;
|
|
186
|
+
}
|
|
187
|
+
return [check];
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// src/editors/cursor.ts
|
|
192
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
193
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
194
|
+
function ruleFrontmatter(input) {
|
|
195
|
+
const desc = input.frontmatter.fields.description;
|
|
196
|
+
if (desc === undefined) {
|
|
197
|
+
throw new Error(`Cursor scaffold requires 'description' in source frontmatter for ${input.base}`);
|
|
198
|
+
}
|
|
199
|
+
return { description: desc, alwaysApply: false };
|
|
200
|
+
}
|
|
201
|
+
function commandFrontmatter(input) {
|
|
202
|
+
const desc = input.frontmatter.fields.description;
|
|
203
|
+
if (desc === undefined) {
|
|
204
|
+
throw new Error(`Cursor scaffold requires 'description' in source frontmatter for ${input.base}`);
|
|
205
|
+
}
|
|
206
|
+
return { description: desc };
|
|
207
|
+
}
|
|
208
|
+
function write(target, content) {
|
|
209
|
+
mkdirSync3(dirname3(target), { recursive: true });
|
|
210
|
+
writeFileSync3(target, content);
|
|
211
|
+
}
|
|
212
|
+
var cursorAdapter = {
|
|
213
|
+
name: "cursor",
|
|
214
|
+
detect(cwd) {
|
|
215
|
+
return existsSync3(join3(cwd, ".cursor"));
|
|
216
|
+
},
|
|
217
|
+
scaffoldSkill(cwd, input) {
|
|
218
|
+
const path = join3(cwd, ".cursor/rules", input.base, "RULE.md");
|
|
219
|
+
write(path, serializeFrontmatter(ruleFrontmatter(input)) + input.body);
|
|
220
|
+
},
|
|
221
|
+
scaffoldCommand(cwd, input) {
|
|
222
|
+
const path = join3(cwd, ".cursor/commands", `${input.base}.md`);
|
|
223
|
+
write(path, serializeFrontmatter(commandFrontmatter(input)) + input.body);
|
|
224
|
+
},
|
|
225
|
+
doctorChecks(cwd, requiredSkills) {
|
|
226
|
+
const rulesDir = join3(cwd, ".cursor/rules");
|
|
227
|
+
const cmdsDir = join3(cwd, ".cursor/commands");
|
|
228
|
+
const missing = [];
|
|
229
|
+
for (const base of requiredSkills) {
|
|
230
|
+
if (!existsSync3(join3(rulesDir, base, "RULE.md")))
|
|
231
|
+
missing.push(`${base}/RULE.md`);
|
|
232
|
+
if (!existsSync3(join3(cmdsDir, `${base}.md`)))
|
|
233
|
+
missing.push(`commands/${base}.md`);
|
|
234
|
+
}
|
|
235
|
+
const check = {
|
|
236
|
+
name: "xera skills present (cursor)",
|
|
237
|
+
ok: missing.length === 0
|
|
238
|
+
};
|
|
239
|
+
if (missing.length)
|
|
240
|
+
check.message = `missing: ${missing.join(", ")} \u2014 run \`xera init --update --editor cursor\``;
|
|
241
|
+
return [check];
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// src/editors/index.ts
|
|
246
|
+
var ALL_EDITORS = ["claude", "cursor", "codex"];
|
|
247
|
+
var editors = {
|
|
248
|
+
claude: claudeAdapter,
|
|
249
|
+
cursor: cursorAdapter,
|
|
250
|
+
codex: codexAdapter
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// src/editors/detect.ts
|
|
254
|
+
import { existsSync as existsSync4 } from "fs";
|
|
255
|
+
import { join as join4 } from "path";
|
|
256
|
+
var MARKERS = {
|
|
257
|
+
claude: ".claude",
|
|
258
|
+
cursor: ".cursor",
|
|
259
|
+
codex: ".agents"
|
|
260
|
+
};
|
|
261
|
+
function detectEditors(cwd) {
|
|
262
|
+
return ALL_EDITORS.filter((name) => existsSync4(join4(cwd, MARKERS[name])));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/checks.ts
|
|
19
266
|
function pushTicketChecks(checks, cwd, ticket, acFieldConfigured) {
|
|
20
|
-
const ticketDir =
|
|
21
|
-
if (!
|
|
267
|
+
const ticketDir = join5(cwd, ".xera", ticket);
|
|
268
|
+
if (!existsSync5(ticketDir)) {
|
|
22
269
|
checks.push({
|
|
23
270
|
name: `${ticket}: .xera/${ticket}/ exists`,
|
|
24
271
|
ok: false,
|
|
@@ -26,8 +273,8 @@ function pushTicketChecks(checks, cwd, ticket, acFieldConfigured) {
|
|
|
26
273
|
});
|
|
27
274
|
return;
|
|
28
275
|
}
|
|
29
|
-
const giPath =
|
|
30
|
-
if (!
|
|
276
|
+
const giPath = join5(ticketDir, "graph-input.json");
|
|
277
|
+
if (!existsSync5(giPath)) {
|
|
31
278
|
checks.push({
|
|
32
279
|
name: `${ticket}: graph-input.json present`,
|
|
33
280
|
ok: false,
|
|
@@ -35,7 +282,7 @@ function pushTicketChecks(checks, cwd, ticket, acFieldConfigured) {
|
|
|
35
282
|
});
|
|
36
283
|
} else {
|
|
37
284
|
try {
|
|
38
|
-
const data = JSON.parse(
|
|
285
|
+
const data = JSON.parse(readFileSync2(giPath, "utf8"));
|
|
39
286
|
if (!Array.isArray(data.modifiesAreas)) {
|
|
40
287
|
checks.push({
|
|
41
288
|
name: `${ticket}: graph-input.json present`,
|
|
@@ -57,8 +304,8 @@ function pushTicketChecks(checks, cwd, ticket, acFieldConfigured) {
|
|
|
57
304
|
});
|
|
58
305
|
}
|
|
59
306
|
}
|
|
60
|
-
const storyPath =
|
|
61
|
-
if (!
|
|
307
|
+
const storyPath = join5(ticketDir, "story.md");
|
|
308
|
+
if (!existsSync5(storyPath)) {
|
|
62
309
|
checks.push({
|
|
63
310
|
name: `${ticket}: story.md acceptanceCriteria`,
|
|
64
311
|
ok: false,
|
|
@@ -66,7 +313,7 @@ function pushTicketChecks(checks, cwd, ticket, acFieldConfigured) {
|
|
|
66
313
|
});
|
|
67
314
|
return;
|
|
68
315
|
}
|
|
69
|
-
const raw =
|
|
316
|
+
const raw = readFileSync2(storyPath, "utf8");
|
|
70
317
|
const m = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
71
318
|
if (!m) {
|
|
72
319
|
checks.push({
|
|
@@ -88,20 +335,29 @@ function pushTicketChecks(checks, cwd, ticket, acFieldConfigured) {
|
|
|
88
335
|
return;
|
|
89
336
|
}
|
|
90
337
|
const ac = Array.isArray(fm.acceptanceCriteria) ? fm.acceptanceCriteria : [];
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
name: `${ticket}: story.md acceptanceCriteria`,
|
|
95
|
-
ok: false,
|
|
96
|
-
message: hint
|
|
97
|
-
});
|
|
98
|
-
} else {
|
|
338
|
+
const source = fm.acceptanceCriteriaSource === "jira-field" || fm.acceptanceCriteriaSource === "body-extraction" || fm.acceptanceCriteriaSource === "none" ? fm.acceptanceCriteriaSource : undefined;
|
|
339
|
+
if (ac.length > 0) {
|
|
340
|
+
const suffix = source ? ` from ${source}` : "";
|
|
99
341
|
checks.push({
|
|
100
342
|
name: `${ticket}: story.md acceptanceCriteria`,
|
|
101
343
|
ok: true,
|
|
102
|
-
message: `${ac.length} AC item(s)`
|
|
344
|
+
message: `${ac.length} AC item(s)${suffix}`
|
|
103
345
|
});
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
let hint;
|
|
349
|
+
if (source === "none") {
|
|
350
|
+
hint = acFieldConfigured ? `jira.fields.acceptanceCriteria is configured but Jira returned no AC for this ticket, and /xera-fetch step 4 found no AC section in the body \u2014 add AC to the Jira ticket` : `AC not in Jira (no custom field configured) and /xera-fetch step 4 found no AC section in the description body \u2014 add AC/DoD to the Jira ticket, or edit story.md frontmatter manually`;
|
|
351
|
+
} else if (source === "body-extraction") {
|
|
352
|
+
hint = `acceptanceCriteriaSource: body-extraction but acceptanceCriteria is empty \u2014 re-run /xera-fetch ${ticket}`;
|
|
353
|
+
} else {
|
|
354
|
+
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. Re-run /xera-fetch ${ticket} so step 4 can extract AC from the body (set jira.fields.acceptanceCriteria in xera.config.ts if your project uses a dedicated Jira field)`;
|
|
104
355
|
}
|
|
356
|
+
checks.push({
|
|
357
|
+
name: `${ticket}: story.md acceptanceCriteria`,
|
|
358
|
+
ok: false,
|
|
359
|
+
message: hint
|
|
360
|
+
});
|
|
105
361
|
}
|
|
106
362
|
async function runChecks(cwd, opts = {}) {
|
|
107
363
|
const checks = [];
|
|
@@ -153,10 +409,10 @@ async function runChecks(cwd, opts = {}) {
|
|
|
153
409
|
});
|
|
154
410
|
}
|
|
155
411
|
}
|
|
156
|
-
const httpAuthDir =
|
|
412
|
+
const httpAuthDir = join5(cwd, ".xera", ".auth", "http");
|
|
157
413
|
for (const role of Object.keys(cfg.http.auth.roles)) {
|
|
158
|
-
const filePath =
|
|
159
|
-
if (!
|
|
414
|
+
const filePath = join5(httpAuthDir, `${role}.json`);
|
|
415
|
+
if (!existsSync5(filePath)) {
|
|
160
416
|
checks.push({
|
|
161
417
|
name: `http auth file present: ${role}`,
|
|
162
418
|
ok: false,
|
|
@@ -215,7 +471,7 @@ async function runChecks(cwd, opts = {}) {
|
|
|
215
471
|
});
|
|
216
472
|
}
|
|
217
473
|
} else {
|
|
218
|
-
const open =
|
|
474
|
+
const open = existsSync5(join5(cwd, spec));
|
|
219
475
|
const openCheck = open ? { name: "OpenAPI spec file present", ok: true } : { name: "OpenAPI spec file present", ok: false, message: `not found at ${spec}` };
|
|
220
476
|
checks.push(openCheck);
|
|
221
477
|
}
|
|
@@ -234,10 +490,10 @@ async function runChecks(cwd, opts = {}) {
|
|
|
234
490
|
message: `${cfg.coverage.staleAfterDays}d is a very large window \u2014 coverage will be slow to react to drift`
|
|
235
491
|
});
|
|
236
492
|
}
|
|
237
|
-
const snapPath =
|
|
238
|
-
if (
|
|
493
|
+
const snapPath = join5(cwd, ".xera/graph/snapshot.json");
|
|
494
|
+
if (existsSync5(snapPath) && cfg.coverage.criticalAreas.length > 0) {
|
|
239
495
|
try {
|
|
240
|
-
const snap = JSON.parse(
|
|
496
|
+
const snap = JSON.parse(readFileSync2(snapPath, "utf8"));
|
|
241
497
|
const known = new Set(Object.keys(snap.areas ?? {}));
|
|
242
498
|
for (const slug of cfg.coverage.criticalAreas) {
|
|
243
499
|
if (!known.has(slug)) {
|
|
@@ -250,9 +506,9 @@ async function runChecks(cwd, opts = {}) {
|
|
|
250
506
|
}
|
|
251
507
|
} catch {}
|
|
252
508
|
}
|
|
253
|
-
if (
|
|
509
|
+
if (existsSync5(snapPath)) {
|
|
254
510
|
try {
|
|
255
|
-
const snap = JSON.parse(
|
|
511
|
+
const snap = JSON.parse(readFileSync2(snapPath, "utf8"));
|
|
256
512
|
const acByTicket = {};
|
|
257
513
|
for (const node of Object.values(snap.acNodes ?? {})) {
|
|
258
514
|
acByTicket[node.ticketId] = (acByTicket[node.ticketId] ?? 0) + 1;
|
|
@@ -280,11 +536,11 @@ async function runChecks(cwd, opts = {}) {
|
|
|
280
536
|
message: String(e.message)
|
|
281
537
|
});
|
|
282
538
|
}
|
|
283
|
-
const envPath =
|
|
284
|
-
if (!
|
|
539
|
+
const envPath = join5(cwd, ".env");
|
|
540
|
+
if (!existsSync5(envPath)) {
|
|
285
541
|
checks.push({ name: ".env present", ok: false, message: "copy from .env.example" });
|
|
286
542
|
} else {
|
|
287
|
-
const env =
|
|
543
|
+
const env = readFileSync2(envPath, "utf8");
|
|
288
544
|
checks.push({ name: "XERA_AUTH_KEY set", ok: /XERA_AUTH_KEY=[0-9a-fA-F]{64}/.test(env) });
|
|
289
545
|
}
|
|
290
546
|
try {
|
|
@@ -297,40 +553,26 @@ async function runChecks(cwd, opts = {}) {
|
|
|
297
553
|
message: "run: bun add -D @playwright/test"
|
|
298
554
|
});
|
|
299
555
|
}
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
556
|
+
const REQUIRED_SKILLS = [
|
|
557
|
+
"xera-run",
|
|
558
|
+
"xera-fetch",
|
|
559
|
+
"xera-feature",
|
|
560
|
+
"xera-script",
|
|
561
|
+
"xera-exec",
|
|
562
|
+
"xera-report",
|
|
563
|
+
"xera-promote"
|
|
564
|
+
];
|
|
565
|
+
const detected = detectEditors(cwd);
|
|
566
|
+
if (detected.length === 0) {
|
|
567
|
+
checks.push({
|
|
568
|
+
name: "xera editor integration present",
|
|
569
|
+
ok: false,
|
|
570
|
+
message: "run `xera init` (scaffolds for Claude Code, Cursor, and/or Codex)"
|
|
571
|
+
});
|
|
303
572
|
} else {
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
"xera-fetch",
|
|
307
|
-
"xera-feature",
|
|
308
|
-
"xera-script",
|
|
309
|
-
"xera-exec",
|
|
310
|
-
"xera-report",
|
|
311
|
-
"xera-promote"
|
|
312
|
-
];
|
|
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
|
-
}
|
|
573
|
+
for (const name of detected) {
|
|
574
|
+
checks.push(...editors[name].doctorChecks(cwd, REQUIRED_SKILLS));
|
|
323
575
|
}
|
|
324
|
-
const skillsCheck = {
|
|
325
|
-
name: "xera skills present",
|
|
326
|
-
ok: missing.length === 0 && legacyFlat.length === 0
|
|
327
|
-
};
|
|
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
|
-
}
|
|
333
|
-
checks.push(skillsCheck);
|
|
334
576
|
}
|
|
335
577
|
return checks;
|
|
336
578
|
}
|
|
@@ -340,7 +582,7 @@ async function doctorCommand(opts) {
|
|
|
340
582
|
const cwd = process.cwd();
|
|
341
583
|
if (opts.logs) {
|
|
342
584
|
const paths = resolveArtifactPaths(cwd, opts.logs);
|
|
343
|
-
if (!
|
|
585
|
+
if (!existsSync6(paths.logPath)) {
|
|
344
586
|
console.log(`No log at ${paths.logPath}`);
|
|
345
587
|
return 0;
|
|
346
588
|
}
|
|
@@ -366,25 +608,47 @@ async function doctorCommand(opts) {
|
|
|
366
608
|
}
|
|
367
609
|
|
|
368
610
|
// src/commands/init.ts
|
|
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";
|
|
611
|
+
import { appendFileSync, existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
377
612
|
import { createRequire } from "module";
|
|
378
|
-
import {
|
|
613
|
+
import { join as join7 } from "path";
|
|
379
614
|
import * as p from "@clack/prompts";
|
|
380
615
|
import { generateKey } from "@xera-ai/core";
|
|
381
616
|
import pc2 from "picocolors";
|
|
382
617
|
|
|
618
|
+
// src/editors/resolve.ts
|
|
619
|
+
function parseFlag(flag) {
|
|
620
|
+
if (flag === "all")
|
|
621
|
+
return [...ALL_EDITORS];
|
|
622
|
+
const parts = flag.split(",").map((s) => s.trim()).filter(Boolean);
|
|
623
|
+
if (parts.length === 0) {
|
|
624
|
+
throw new Error(`--editor: empty value. Valid: ${ALL_EDITORS.join(", ")} (or 'all').`);
|
|
625
|
+
}
|
|
626
|
+
const bad = parts.filter((p) => !ALL_EDITORS.includes(p));
|
|
627
|
+
if (bad.length) {
|
|
628
|
+
throw new Error(`--editor: unknown value(s) [${bad.join(", ")}]. Valid: ${ALL_EDITORS.join(", ")} (or 'all').`);
|
|
629
|
+
}
|
|
630
|
+
return parts;
|
|
631
|
+
}
|
|
632
|
+
async function resolveEditors(opts) {
|
|
633
|
+
if (opts.flag !== undefined)
|
|
634
|
+
return parseFlag(opts.flag);
|
|
635
|
+
const detected = detectEditors(opts.cwd);
|
|
636
|
+
if (opts.isUpdate)
|
|
637
|
+
return detected;
|
|
638
|
+
if (detected.length > 0)
|
|
639
|
+
return detected;
|
|
640
|
+
if (opts.isYes)
|
|
641
|
+
return [...ALL_EDITORS];
|
|
642
|
+
if (opts.prompt)
|
|
643
|
+
return opts.prompt();
|
|
644
|
+
return [...ALL_EDITORS];
|
|
645
|
+
}
|
|
646
|
+
|
|
383
647
|
// src/scaffold.ts
|
|
384
|
-
import { mkdirSync, readdirSync, readFileSync as
|
|
385
|
-
import { dirname, join as
|
|
648
|
+
import { mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
649
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
386
650
|
import { fileURLToPath } from "url";
|
|
387
|
-
var TEMPLATE_ROOT =
|
|
651
|
+
var TEMPLATE_ROOT = join6(fileURLToPath(import.meta.url), "..", "..", "templates");
|
|
388
652
|
function render(tmpl, vars) {
|
|
389
653
|
let out = tmpl;
|
|
390
654
|
out = out.replace(/\{\{#each (\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (_, k, block) => {
|
|
@@ -401,9 +665,9 @@ function render(tmpl, vars) {
|
|
|
401
665
|
return out;
|
|
402
666
|
}
|
|
403
667
|
function scaffoldFile(targetPath, templateName, vars) {
|
|
404
|
-
const tmpl =
|
|
405
|
-
|
|
406
|
-
|
|
668
|
+
const tmpl = readFileSync3(join6(TEMPLATE_ROOT, templateName), "utf8");
|
|
669
|
+
mkdirSync4(dirname4(targetPath), { recursive: true });
|
|
670
|
+
writeFileSync4(targetPath, render(tmpl, vars));
|
|
407
671
|
}
|
|
408
672
|
|
|
409
673
|
// src/commands/init.ts
|
|
@@ -508,26 +772,26 @@ async function initCommand(opts) {
|
|
|
508
772
|
authKey: generateKey()
|
|
509
773
|
};
|
|
510
774
|
const configTmpl = shape === "web" ? "xera.config.ts.tmpl" : shape === "api" ? "http-xera.config.ts.tmpl" : "mixed-xera.config.ts.tmpl";
|
|
511
|
-
scaffoldFile(
|
|
775
|
+
scaffoldFile(join7(cwd, "xera.config.ts"), configTmpl, vars);
|
|
512
776
|
const pwTmpl = shape === "api" ? "http-playwright.config.ts.tmpl" : "playwright.config.ts.tmpl";
|
|
513
|
-
scaffoldFile(
|
|
514
|
-
scaffoldFile(
|
|
777
|
+
scaffoldFile(join7(cwd, "playwright.config.ts"), pwTmpl, vars);
|
|
778
|
+
scaffoldFile(join7(cwd, "tsconfig.json"), "tsconfig.json.tmpl", vars);
|
|
515
779
|
if (shape === "api") {
|
|
516
|
-
scaffoldFile(
|
|
780
|
+
scaffoldFile(join7(cwd, ".env.example"), "http-env.example.tmpl", vars);
|
|
517
781
|
} else {
|
|
518
|
-
scaffoldFile(
|
|
782
|
+
scaffoldFile(join7(cwd, ".env.example"), "env.example.tmpl", vars);
|
|
519
783
|
}
|
|
520
784
|
if (wantsWeb || wantsHttp) {
|
|
521
|
-
scaffoldFile(
|
|
785
|
+
scaffoldFile(join7(cwd, "shared/auth-setup.ts"), "auth-setup.ts.tmpl", vars);
|
|
522
786
|
}
|
|
523
|
-
scaffoldFile(
|
|
787
|
+
scaffoldFile(join7(cwd, ".github/workflows/xera-graph.yml"), "xera-graph.yml.template", vars);
|
|
524
788
|
if (wantsHttp && vars.openapiPath && !vars.openapiPath.startsWith("http")) {
|
|
525
|
-
const openapiTarget =
|
|
526
|
-
if (!
|
|
789
|
+
const openapiTarget = join7(cwd, vars.openapiPath);
|
|
790
|
+
if (!existsSync7(openapiTarget)) {
|
|
527
791
|
scaffoldFile(openapiTarget, "openapi.yaml.tmpl", vars);
|
|
528
792
|
}
|
|
529
793
|
}
|
|
530
|
-
const gitignorePath =
|
|
794
|
+
const gitignorePath = join7(cwd, ".gitignore");
|
|
531
795
|
const gitignoreAdditions = [
|
|
532
796
|
"",
|
|
533
797
|
"# xera",
|
|
@@ -539,33 +803,55 @@ async function initCommand(opts) {
|
|
|
539
803
|
"node_modules/"
|
|
540
804
|
].join(`
|
|
541
805
|
`);
|
|
542
|
-
if (
|
|
543
|
-
const current =
|
|
806
|
+
if (existsSync7(gitignorePath)) {
|
|
807
|
+
const current = readFileSync4(gitignorePath, "utf8");
|
|
544
808
|
if (!current.includes("# xera"))
|
|
545
809
|
appendFileSync(gitignorePath, gitignoreAdditions);
|
|
546
810
|
} else {
|
|
547
|
-
|
|
811
|
+
writeFileSync5(gitignorePath, `${gitignoreAdditions.trim()}
|
|
548
812
|
`);
|
|
549
813
|
}
|
|
814
|
+
const editorTargets = await resolveEditors({
|
|
815
|
+
flag: opts.editor,
|
|
816
|
+
cwd,
|
|
817
|
+
isUpdate: false,
|
|
818
|
+
isYes: opts.yes,
|
|
819
|
+
prompt: async () => {
|
|
820
|
+
const choice = await p.multiselect({
|
|
821
|
+
message: "Which editor(s) should xera scaffold for?",
|
|
822
|
+
options: [
|
|
823
|
+
{ value: "claude", label: "Claude Code (.claude/skills/, .claude/commands/)" },
|
|
824
|
+
{ value: "cursor", label: "Cursor (.cursor/rules/, .cursor/commands/)" },
|
|
825
|
+
{ value: "codex", label: "OpenAI Codex CLI (.agents/skills/)" }
|
|
826
|
+
],
|
|
827
|
+
initialValues: ["claude"],
|
|
828
|
+
required: true
|
|
829
|
+
});
|
|
830
|
+
if (typeof choice === "symbol")
|
|
831
|
+
cancel2();
|
|
832
|
+
return choice;
|
|
833
|
+
}
|
|
834
|
+
});
|
|
550
835
|
const skillsPkgPath = require2.resolve("@xera-ai/skills/package.json");
|
|
551
|
-
const skillsSrcDir =
|
|
836
|
+
const skillsSrcDir = join7(skillsPkgPath, "..");
|
|
552
837
|
const SKILL_IGNORE = new Set(["package.json", "version.json", "CHANGELOG.md"]);
|
|
553
838
|
for (const name of readdirSync2(skillsSrcDir)) {
|
|
554
839
|
if (SKILL_IGNORE.has(name))
|
|
555
840
|
continue;
|
|
556
841
|
if (!name.endsWith(".md"))
|
|
557
842
|
continue;
|
|
558
|
-
const
|
|
843
|
+
const raw = readFileSync4(join7(skillsSrcDir, name), "utf8");
|
|
844
|
+
const { frontmatter, body } = parseFrontmatter(raw);
|
|
559
845
|
const base = name.replace(/\.md$/, "");
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
846
|
+
const skillInput = { base, body, frontmatter };
|
|
847
|
+
for (const editorName of editorTargets) {
|
|
848
|
+
const adapter = editors[editorName];
|
|
849
|
+
adapter.scaffoldSkill(cwd, skillInput);
|
|
850
|
+
adapter.scaffoldCommand?.(cwd, skillInput);
|
|
851
|
+
}
|
|
566
852
|
}
|
|
567
|
-
const pkgPath =
|
|
568
|
-
const pkg =
|
|
853
|
+
const pkgPath = join7(cwd, "package.json");
|
|
854
|
+
const pkg = existsSync7(pkgPath) ? JSON.parse(readFileSync4(pkgPath, "utf8")) : { name: "xera-project", private: true, type: "module" };
|
|
569
855
|
pkg.scripts = pkg.scripts ?? {};
|
|
570
856
|
pkg.scripts["xera:fetch"] = "xera-internal fetch";
|
|
571
857
|
pkg.scripts["xera:validate-feature"] = "xera-internal validate-feature";
|
|
@@ -601,7 +887,17 @@ async function initCommand(opts) {
|
|
|
601
887
|
pkg.devDependencies["@playwright/test"] = "^1.60.0";
|
|
602
888
|
pkg.devDependencies["@types/node"] = "^25.8.0";
|
|
603
889
|
pkg.devDependencies["typescript"] = "^6.0.3";
|
|
604
|
-
|
|
890
|
+
writeFileSync5(pkgPath, JSON.stringify(pkg, null, 2));
|
|
891
|
+
const editorLines = editorTargets.map((e) => {
|
|
892
|
+
if (e === "claude")
|
|
893
|
+
return " Claude Code: /xera-run <TICKET>";
|
|
894
|
+
if (e === "cursor")
|
|
895
|
+
return " Cursor: /xera-run <TICKET> (slash menu)";
|
|
896
|
+
if (e === "codex")
|
|
897
|
+
return ' OpenAI Codex CLI: type "run xera for <TICKET>" \u2014 Codex picks up the xera-run skill';
|
|
898
|
+
return "";
|
|
899
|
+
}).join(`
|
|
900
|
+
`);
|
|
605
901
|
const nextSteps = shape === "api" ? `
|
|
606
902
|
Next:
|
|
607
903
|
1) Copy .env.example to .env and set your auth credentials:
|
|
@@ -610,7 +906,7 @@ Next:
|
|
|
610
906
|
2) Run pre-authentication:
|
|
611
907
|
bun run xera:auth-setup
|
|
612
908
|
3) Start testing:
|
|
613
|
-
|
|
909
|
+
${editorLines}
|
|
614
910
|
` : shape === "mixed" ? `
|
|
615
911
|
Next:
|
|
616
912
|
1) Copy .env.example to .env and set credentials (both web logins and API tokens):
|
|
@@ -618,7 +914,7 @@ Next:
|
|
|
618
914
|
2) Run pre-authentication:
|
|
619
915
|
bun run xera:auth-setup
|
|
620
916
|
3) Start testing:
|
|
621
|
-
|
|
917
|
+
${editorLines}
|
|
622
918
|
` : `
|
|
623
919
|
Next:
|
|
624
920
|
1) Copy .env.example to .env and set your Jira credentials:
|
|
@@ -626,7 +922,7 @@ Next:
|
|
|
626
922
|
2) Run pre-authentication:
|
|
627
923
|
bun run xera:auth-setup
|
|
628
924
|
3) Start testing:
|
|
629
|
-
|
|
925
|
+
${editorLines}
|
|
630
926
|
`;
|
|
631
927
|
p.note(nextSteps.trim(), "Next steps");
|
|
632
928
|
p.outro(pc2.green("xera initialized!"));
|
|
@@ -635,24 +931,23 @@ Next:
|
|
|
635
931
|
// src/commands/init-update.ts
|
|
636
932
|
import {
|
|
637
933
|
copyFileSync,
|
|
638
|
-
existsSync as
|
|
639
|
-
mkdirSync as
|
|
934
|
+
existsSync as existsSync8,
|
|
935
|
+
mkdirSync as mkdirSync5,
|
|
640
936
|
readdirSync as readdirSync3,
|
|
641
|
-
readFileSync as
|
|
642
|
-
|
|
643
|
-
writeFileSync as writeFileSync3
|
|
937
|
+
readFileSync as readFileSync5,
|
|
938
|
+
writeFileSync as writeFileSync6
|
|
644
939
|
} from "fs";
|
|
645
940
|
import { createRequire as createRequire2 } from "module";
|
|
646
|
-
import {
|
|
941
|
+
import { join as join8 } from "path";
|
|
647
942
|
import * as p2 from "@clack/prompts";
|
|
648
943
|
import pc3 from "picocolors";
|
|
649
944
|
var require3 = createRequire2(import.meta.url);
|
|
650
945
|
var CLI_VERSION2 = require3("../package.json").version;
|
|
651
946
|
function detectAdaptersFromConfig(cwd) {
|
|
652
|
-
const configPath =
|
|
653
|
-
if (!
|
|
947
|
+
const configPath = join8(cwd, "xera.config.ts");
|
|
948
|
+
if (!existsSync8(configPath))
|
|
654
949
|
return null;
|
|
655
|
-
const cfg =
|
|
950
|
+
const cfg = readFileSync5(configPath, "utf8");
|
|
656
951
|
const m = cfg.match(/adapters:\s*\[([^\]]+)\]/);
|
|
657
952
|
if (!m)
|
|
658
953
|
return null;
|
|
@@ -703,7 +998,7 @@ ${roles.map((r) => ` ${r}: { envEmail: 'TEST_${r.toUpperCase().replace(/-
|
|
|
703
998
|
` web: {`,
|
|
704
999
|
` baseUrl: { staging: '${baseUrl}' },`,
|
|
705
1000
|
` defaultEnv: 'staging',`,
|
|
706
|
-
authBlock
|
|
1001
|
+
`${authBlock} },`
|
|
707
1002
|
].join(`
|
|
708
1003
|
`);
|
|
709
1004
|
}
|
|
@@ -729,12 +1024,12 @@ export const web = defineAuthSetup(async (page, _role, creds) => {
|
|
|
729
1024
|
async function initUpdateCommand(opts) {
|
|
730
1025
|
const cwd = process.cwd();
|
|
731
1026
|
p2.intro(pc3.cyan("xera init --update"));
|
|
732
|
-
const pkgPath =
|
|
733
|
-
if (!
|
|
1027
|
+
const pkgPath = join8(cwd, "package.json");
|
|
1028
|
+
if (!existsSync8(pkgPath)) {
|
|
734
1029
|
p2.cancel("No package.json found \u2014 run `xera init` first.");
|
|
735
1030
|
process.exit(1);
|
|
736
1031
|
}
|
|
737
|
-
const pkg = JSON.parse(
|
|
1032
|
+
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
738
1033
|
pkg.dependencies = pkg.dependencies ?? {};
|
|
739
1034
|
pkg.dependencies["@xera-ai/core"] = `^${CLI_VERSION2}`;
|
|
740
1035
|
pkg.dependencies["@xera-ai/prompts"] = `^${CLI_VERSION2}`;
|
|
@@ -754,79 +1049,47 @@ async function initUpdateCommand(opts) {
|
|
|
754
1049
|
pkg.scripts["xera:impact-prepare"] = "xera-internal impact-prepare";
|
|
755
1050
|
pkg.scripts["xera:heal-prepare"] = "xera-internal heal-prepare";
|
|
756
1051
|
pkg.scripts["xera:disputes"] = "xera-internal disputes";
|
|
757
|
-
|
|
758
|
-
const wfDir =
|
|
759
|
-
|
|
1052
|
+
writeFileSync6(pkgPath, JSON.stringify(pkg, null, 2));
|
|
1053
|
+
const wfDir = join8(cwd, ".github/workflows");
|
|
1054
|
+
mkdirSync5(wfDir, { recursive: true });
|
|
760
1055
|
try {
|
|
761
1056
|
const cliPkgPath = require3.resolve("@xera-ai/cli/package.json");
|
|
762
|
-
const cliTplPath =
|
|
763
|
-
copyFileSync(cliTplPath,
|
|
1057
|
+
const cliTplPath = join8(cliPkgPath, "..", "templates/xera-graph.yml.template");
|
|
1058
|
+
copyFileSync(cliTplPath, join8(wfDir, "xera-graph.yml"));
|
|
764
1059
|
p2.log.info("scaffolded .github/workflows/xera-graph.yml");
|
|
765
1060
|
} catch (_e) {
|
|
766
1061
|
p2.log.warn("skipped xera-graph.yml scaffold (re-run `xera init` to create it)");
|
|
767
1062
|
}
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const
|
|
778
|
-
const
|
|
779
|
-
const
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
if (targets.every((s) => s.state === "missing")) {
|
|
799
|
-
for (const { path } of targets) {
|
|
800
|
-
mkdirSync3(dirname3(path), { recursive: true });
|
|
801
|
-
writeFileSync3(path, newContent);
|
|
802
|
-
}
|
|
803
|
-
p2.log.info(`+ ${name}`);
|
|
804
|
-
continue;
|
|
805
|
-
}
|
|
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}`);
|
|
811
|
-
continue;
|
|
812
|
-
}
|
|
813
|
-
const choice = await p2.select({
|
|
814
|
-
message: `${name} differs from package version`,
|
|
815
|
-
options: [
|
|
816
|
-
{ value: "keep", label: "Keep local" },
|
|
817
|
-
{ value: "overwrite", label: "Overwrite with package version" }
|
|
818
|
-
]
|
|
819
|
-
});
|
|
820
|
-
if (choice === "overwrite") {
|
|
821
|
-
for (const { path } of targets) {
|
|
822
|
-
mkdirSync3(dirname3(path), { recursive: true });
|
|
823
|
-
writeFileSync3(path, newContent);
|
|
1063
|
+
const editorTargets = await resolveEditors({
|
|
1064
|
+
flag: opts.editor,
|
|
1065
|
+
cwd,
|
|
1066
|
+
isUpdate: true,
|
|
1067
|
+
isYes: opts.yes
|
|
1068
|
+
});
|
|
1069
|
+
if (editorTargets.length === 0) {
|
|
1070
|
+
p2.log.warn("No editor integration detected in this project. Pass --editor claude|cursor|codex|all to add one.");
|
|
1071
|
+
} else {
|
|
1072
|
+
const skillsSrc = require3.resolve("@xera-ai/skills/package.json");
|
|
1073
|
+
const newSkillsDir = join8(skillsSrc, "..");
|
|
1074
|
+
const SKILL_IGNORE = new Set(["package.json", "version.json", "CHANGELOG.md"]);
|
|
1075
|
+
for (const name of readdirSync3(newSkillsDir)) {
|
|
1076
|
+
if (SKILL_IGNORE.has(name))
|
|
1077
|
+
continue;
|
|
1078
|
+
if (!name.endsWith(".md"))
|
|
1079
|
+
continue;
|
|
1080
|
+
const rawNew = readFileSync5(join8(newSkillsDir, name), "utf8");
|
|
1081
|
+
const { frontmatter, body } = parseFrontmatter(rawNew);
|
|
1082
|
+
const base = name.replace(/\.md$/, "");
|
|
1083
|
+
const skillInput = { base, body, frontmatter };
|
|
1084
|
+
for (const editorName of editorTargets) {
|
|
1085
|
+
const adapter = editors[editorName];
|
|
1086
|
+
const migrated = adapter.legacyMigrate?.(cwd, base) ?? false;
|
|
1087
|
+
if (migrated)
|
|
1088
|
+
p2.log.success(`migrated ${base} (${editorName}) to new layout`);
|
|
1089
|
+
adapter.scaffoldSkill(cwd, skillInput);
|
|
1090
|
+
adapter.scaffoldCommand?.(cwd, skillInput);
|
|
824
1091
|
}
|
|
825
|
-
p2.log.
|
|
826
|
-
} else {
|
|
827
|
-
if (migratedLegacy)
|
|
828
|
-
p2.log.success(`migrated ${name} to .claude/skills/${base}/SKILL.md`);
|
|
829
|
-
p2.log.warn(`kept local ${name}`);
|
|
1092
|
+
p2.log.info(`refreshed ${base} across [${editorTargets.join(", ")}]`);
|
|
830
1093
|
}
|
|
831
1094
|
}
|
|
832
1095
|
const hasShapeFlags = opts.apiBaseUrl !== undefined || opts.openapiPath !== undefined || opts.authStrategy !== undefined || opts.httpRoles !== undefined || opts.stagingUrl !== undefined || opts.authEnabled !== undefined || opts.roles !== undefined;
|
|
@@ -924,7 +1187,7 @@ async function main() {
|
|
|
924
1187
|
cli.help();
|
|
925
1188
|
cli.version(VERSION);
|
|
926
1189
|
cli.usage("<command> [options]");
|
|
927
|
-
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) => {
|
|
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) => {
|
|
928
1191
|
if (opts.update) {
|
|
929
1192
|
const updateOpts = { yes: !!opts.yes };
|
|
930
1193
|
if (opts.shape !== undefined) {
|
|
@@ -957,6 +1220,8 @@ async function main() {
|
|
|
957
1220
|
updateOpts.authEnabled = opts.authEnabled;
|
|
958
1221
|
if (opts.roles !== undefined)
|
|
959
1222
|
updateOpts.roles = opts.roles;
|
|
1223
|
+
if (opts.editor !== undefined)
|
|
1224
|
+
updateOpts.editor = opts.editor;
|
|
960
1225
|
await initUpdateCommand(updateOpts);
|
|
961
1226
|
return;
|
|
962
1227
|
}
|
|
@@ -999,6 +1264,8 @@ async function main() {
|
|
|
999
1264
|
initOpts.openapiPath = opts.openapiPath;
|
|
1000
1265
|
if (opts.httpRoles !== undefined)
|
|
1001
1266
|
initOpts.httpRoles = opts.httpRoles;
|
|
1267
|
+
if (opts.editor !== undefined)
|
|
1268
|
+
initOpts.editor = opts.editor;
|
|
1002
1269
|
await initCommand(initOpts);
|
|
1003
1270
|
});
|
|
1004
1271
|
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.13.1",
|
|
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.13.1",
|
|
19
|
+
"@xera-ai/skills": "^0.13.1",
|
|
20
20
|
"@clack/prompts": "1.4.0",
|
|
21
21
|
"cac": "7.0.0",
|
|
22
22
|
"picocolors": "1.1.1",
|