@xera-ai/cli 0.13.0 → 0.14.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 +436 -178
- 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({
|
|
@@ -162,10 +409,10 @@ async function runChecks(cwd, opts = {}) {
|
|
|
162
409
|
});
|
|
163
410
|
}
|
|
164
411
|
}
|
|
165
|
-
const httpAuthDir =
|
|
412
|
+
const httpAuthDir = join5(cwd, ".xera", ".auth", "http");
|
|
166
413
|
for (const role of Object.keys(cfg.http.auth.roles)) {
|
|
167
|
-
const filePath =
|
|
168
|
-
if (!
|
|
414
|
+
const filePath = join5(httpAuthDir, `${role}.json`);
|
|
415
|
+
if (!existsSync5(filePath)) {
|
|
169
416
|
checks.push({
|
|
170
417
|
name: `http auth file present: ${role}`,
|
|
171
418
|
ok: false,
|
|
@@ -224,7 +471,7 @@ async function runChecks(cwd, opts = {}) {
|
|
|
224
471
|
});
|
|
225
472
|
}
|
|
226
473
|
} else {
|
|
227
|
-
const open =
|
|
474
|
+
const open = existsSync5(join5(cwd, spec));
|
|
228
475
|
const openCheck = open ? { name: "OpenAPI spec file present", ok: true } : { name: "OpenAPI spec file present", ok: false, message: `not found at ${spec}` };
|
|
229
476
|
checks.push(openCheck);
|
|
230
477
|
}
|
|
@@ -243,10 +490,10 @@ async function runChecks(cwd, opts = {}) {
|
|
|
243
490
|
message: `${cfg.coverage.staleAfterDays}d is a very large window \u2014 coverage will be slow to react to drift`
|
|
244
491
|
});
|
|
245
492
|
}
|
|
246
|
-
const snapPath =
|
|
247
|
-
if (
|
|
493
|
+
const snapPath = join5(cwd, ".xera/graph/snapshot.json");
|
|
494
|
+
if (existsSync5(snapPath) && cfg.coverage.criticalAreas.length > 0) {
|
|
248
495
|
try {
|
|
249
|
-
const snap = JSON.parse(
|
|
496
|
+
const snap = JSON.parse(readFileSync2(snapPath, "utf8"));
|
|
250
497
|
const known = new Set(Object.keys(snap.areas ?? {}));
|
|
251
498
|
for (const slug of cfg.coverage.criticalAreas) {
|
|
252
499
|
if (!known.has(slug)) {
|
|
@@ -259,9 +506,9 @@ async function runChecks(cwd, opts = {}) {
|
|
|
259
506
|
}
|
|
260
507
|
} catch {}
|
|
261
508
|
}
|
|
262
|
-
if (
|
|
509
|
+
if (existsSync5(snapPath)) {
|
|
263
510
|
try {
|
|
264
|
-
const snap = JSON.parse(
|
|
511
|
+
const snap = JSON.parse(readFileSync2(snapPath, "utf8"));
|
|
265
512
|
const acByTicket = {};
|
|
266
513
|
for (const node of Object.values(snap.acNodes ?? {})) {
|
|
267
514
|
acByTicket[node.ticketId] = (acByTicket[node.ticketId] ?? 0) + 1;
|
|
@@ -289,11 +536,11 @@ async function runChecks(cwd, opts = {}) {
|
|
|
289
536
|
message: String(e.message)
|
|
290
537
|
});
|
|
291
538
|
}
|
|
292
|
-
const envPath =
|
|
293
|
-
if (!
|
|
539
|
+
const envPath = join5(cwd, ".env");
|
|
540
|
+
if (!existsSync5(envPath)) {
|
|
294
541
|
checks.push({ name: ".env present", ok: false, message: "copy from .env.example" });
|
|
295
542
|
} else {
|
|
296
|
-
const env =
|
|
543
|
+
const env = readFileSync2(envPath, "utf8");
|
|
297
544
|
checks.push({ name: "XERA_AUTH_KEY set", ok: /XERA_AUTH_KEY=[0-9a-fA-F]{64}/.test(env) });
|
|
298
545
|
}
|
|
299
546
|
try {
|
|
@@ -306,40 +553,26 @@ async function runChecks(cwd, opts = {}) {
|
|
|
306
553
|
message: "run: bun add -D @playwright/test"
|
|
307
554
|
});
|
|
308
555
|
}
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
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
|
+
});
|
|
312
572
|
} else {
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
"xera-fetch",
|
|
316
|
-
"xera-feature",
|
|
317
|
-
"xera-script",
|
|
318
|
-
"xera-exec",
|
|
319
|
-
"xera-report",
|
|
320
|
-
"xera-promote"
|
|
321
|
-
];
|
|
322
|
-
const missing = [];
|
|
323
|
-
const legacyFlat = [];
|
|
324
|
-
for (const base of required) {
|
|
325
|
-
if (existsSync(join(skillsDir, base, "SKILL.md")))
|
|
326
|
-
continue;
|
|
327
|
-
if (existsSync(join(skillsDir, `${base}.md`))) {
|
|
328
|
-
legacyFlat.push(base);
|
|
329
|
-
} else {
|
|
330
|
-
missing.push(base);
|
|
331
|
-
}
|
|
573
|
+
for (const name of detected) {
|
|
574
|
+
checks.push(...editors[name].doctorChecks(cwd, REQUIRED_SKILLS));
|
|
332
575
|
}
|
|
333
|
-
const skillsCheck = {
|
|
334
|
-
name: "xera skills present",
|
|
335
|
-
ok: missing.length === 0 && legacyFlat.length === 0
|
|
336
|
-
};
|
|
337
|
-
if (missing.length) {
|
|
338
|
-
skillsCheck.message = `missing: ${missing.map((b) => `${b}/SKILL.md`).join(", ")}`;
|
|
339
|
-
} else if (legacyFlat.length) {
|
|
340
|
-
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)`;
|
|
341
|
-
}
|
|
342
|
-
checks.push(skillsCheck);
|
|
343
576
|
}
|
|
344
577
|
return checks;
|
|
345
578
|
}
|
|
@@ -349,7 +582,7 @@ async function doctorCommand(opts) {
|
|
|
349
582
|
const cwd = process.cwd();
|
|
350
583
|
if (opts.logs) {
|
|
351
584
|
const paths = resolveArtifactPaths(cwd, opts.logs);
|
|
352
|
-
if (!
|
|
585
|
+
if (!existsSync6(paths.logPath)) {
|
|
353
586
|
console.log(`No log at ${paths.logPath}`);
|
|
354
587
|
return 0;
|
|
355
588
|
}
|
|
@@ -375,25 +608,47 @@ async function doctorCommand(opts) {
|
|
|
375
608
|
}
|
|
376
609
|
|
|
377
610
|
// src/commands/init.ts
|
|
378
|
-
import {
|
|
379
|
-
appendFileSync,
|
|
380
|
-
existsSync as existsSync3,
|
|
381
|
-
mkdirSync as mkdirSync2,
|
|
382
|
-
readdirSync as readdirSync2,
|
|
383
|
-
readFileSync as readFileSync3,
|
|
384
|
-
writeFileSync as writeFileSync2
|
|
385
|
-
} from "fs";
|
|
611
|
+
import { appendFileSync, existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
386
612
|
import { createRequire } from "module";
|
|
387
|
-
import {
|
|
613
|
+
import { join as join7 } from "path";
|
|
388
614
|
import * as p from "@clack/prompts";
|
|
389
615
|
import { generateKey } from "@xera-ai/core";
|
|
390
616
|
import pc2 from "picocolors";
|
|
391
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
|
+
|
|
392
647
|
// src/scaffold.ts
|
|
393
|
-
import { mkdirSync, readdirSync, readFileSync as
|
|
394
|
-
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";
|
|
395
650
|
import { fileURLToPath } from "url";
|
|
396
|
-
var TEMPLATE_ROOT =
|
|
651
|
+
var TEMPLATE_ROOT = join6(fileURLToPath(import.meta.url), "..", "..", "templates");
|
|
397
652
|
function render(tmpl, vars) {
|
|
398
653
|
let out = tmpl;
|
|
399
654
|
out = out.replace(/\{\{#each (\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (_, k, block) => {
|
|
@@ -410,9 +665,9 @@ function render(tmpl, vars) {
|
|
|
410
665
|
return out;
|
|
411
666
|
}
|
|
412
667
|
function scaffoldFile(targetPath, templateName, vars) {
|
|
413
|
-
const tmpl =
|
|
414
|
-
|
|
415
|
-
|
|
668
|
+
const tmpl = readFileSync3(join6(TEMPLATE_ROOT, templateName), "utf8");
|
|
669
|
+
mkdirSync4(dirname4(targetPath), { recursive: true });
|
|
670
|
+
writeFileSync4(targetPath, render(tmpl, vars));
|
|
416
671
|
}
|
|
417
672
|
|
|
418
673
|
// src/commands/init.ts
|
|
@@ -517,26 +772,26 @@ async function initCommand(opts) {
|
|
|
517
772
|
authKey: generateKey()
|
|
518
773
|
};
|
|
519
774
|
const configTmpl = shape === "web" ? "xera.config.ts.tmpl" : shape === "api" ? "http-xera.config.ts.tmpl" : "mixed-xera.config.ts.tmpl";
|
|
520
|
-
scaffoldFile(
|
|
775
|
+
scaffoldFile(join7(cwd, "xera.config.ts"), configTmpl, vars);
|
|
521
776
|
const pwTmpl = shape === "api" ? "http-playwright.config.ts.tmpl" : "playwright.config.ts.tmpl";
|
|
522
|
-
scaffoldFile(
|
|
523
|
-
scaffoldFile(
|
|
777
|
+
scaffoldFile(join7(cwd, "playwright.config.ts"), pwTmpl, vars);
|
|
778
|
+
scaffoldFile(join7(cwd, "tsconfig.json"), "tsconfig.json.tmpl", vars);
|
|
524
779
|
if (shape === "api") {
|
|
525
|
-
scaffoldFile(
|
|
780
|
+
scaffoldFile(join7(cwd, ".env.example"), "http-env.example.tmpl", vars);
|
|
526
781
|
} else {
|
|
527
|
-
scaffoldFile(
|
|
782
|
+
scaffoldFile(join7(cwd, ".env.example"), "env.example.tmpl", vars);
|
|
528
783
|
}
|
|
529
784
|
if (wantsWeb || wantsHttp) {
|
|
530
|
-
scaffoldFile(
|
|
785
|
+
scaffoldFile(join7(cwd, "shared/auth-setup.ts"), "auth-setup.ts.tmpl", vars);
|
|
531
786
|
}
|
|
532
|
-
scaffoldFile(
|
|
787
|
+
scaffoldFile(join7(cwd, ".github/workflows/xera-graph.yml"), "xera-graph.yml.template", vars);
|
|
533
788
|
if (wantsHttp && vars.openapiPath && !vars.openapiPath.startsWith("http")) {
|
|
534
|
-
const openapiTarget =
|
|
535
|
-
if (!
|
|
789
|
+
const openapiTarget = join7(cwd, vars.openapiPath);
|
|
790
|
+
if (!existsSync7(openapiTarget)) {
|
|
536
791
|
scaffoldFile(openapiTarget, "openapi.yaml.tmpl", vars);
|
|
537
792
|
}
|
|
538
793
|
}
|
|
539
|
-
const gitignorePath =
|
|
794
|
+
const gitignorePath = join7(cwd, ".gitignore");
|
|
540
795
|
const gitignoreAdditions = [
|
|
541
796
|
"",
|
|
542
797
|
"# xera",
|
|
@@ -548,33 +803,55 @@ async function initCommand(opts) {
|
|
|
548
803
|
"node_modules/"
|
|
549
804
|
].join(`
|
|
550
805
|
`);
|
|
551
|
-
if (
|
|
552
|
-
const current =
|
|
806
|
+
if (existsSync7(gitignorePath)) {
|
|
807
|
+
const current = readFileSync4(gitignorePath, "utf8");
|
|
553
808
|
if (!current.includes("# xera"))
|
|
554
809
|
appendFileSync(gitignorePath, gitignoreAdditions);
|
|
555
810
|
} else {
|
|
556
|
-
|
|
811
|
+
writeFileSync5(gitignorePath, `${gitignoreAdditions.trim()}
|
|
557
812
|
`);
|
|
558
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
|
+
});
|
|
559
835
|
const skillsPkgPath = require2.resolve("@xera-ai/skills/package.json");
|
|
560
|
-
const skillsSrcDir =
|
|
836
|
+
const skillsSrcDir = join7(skillsPkgPath, "..");
|
|
561
837
|
const SKILL_IGNORE = new Set(["package.json", "version.json", "CHANGELOG.md"]);
|
|
562
838
|
for (const name of readdirSync2(skillsSrcDir)) {
|
|
563
839
|
if (SKILL_IGNORE.has(name))
|
|
564
840
|
continue;
|
|
565
841
|
if (!name.endsWith(".md"))
|
|
566
842
|
continue;
|
|
567
|
-
const
|
|
843
|
+
const raw = readFileSync4(join7(skillsSrcDir, name), "utf8");
|
|
844
|
+
const { frontmatter, body } = parseFrontmatter(raw);
|
|
568
845
|
const base = name.replace(/\.md$/, "");
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
+
}
|
|
575
852
|
}
|
|
576
|
-
const pkgPath =
|
|
577
|
-
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" };
|
|
578
855
|
pkg.scripts = pkg.scripts ?? {};
|
|
579
856
|
pkg.scripts["xera:fetch"] = "xera-internal fetch";
|
|
580
857
|
pkg.scripts["xera:validate-feature"] = "xera-internal validate-feature";
|
|
@@ -610,7 +887,17 @@ async function initCommand(opts) {
|
|
|
610
887
|
pkg.devDependencies["@playwright/test"] = "^1.60.0";
|
|
611
888
|
pkg.devDependencies["@types/node"] = "^25.8.0";
|
|
612
889
|
pkg.devDependencies["typescript"] = "^6.0.3";
|
|
613
|
-
|
|
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
|
+
`);
|
|
614
901
|
const nextSteps = shape === "api" ? `
|
|
615
902
|
Next:
|
|
616
903
|
1) Copy .env.example to .env and set your auth credentials:
|
|
@@ -619,7 +906,7 @@ Next:
|
|
|
619
906
|
2) Run pre-authentication:
|
|
620
907
|
bun run xera:auth-setup
|
|
621
908
|
3) Start testing:
|
|
622
|
-
|
|
909
|
+
${editorLines}
|
|
623
910
|
` : shape === "mixed" ? `
|
|
624
911
|
Next:
|
|
625
912
|
1) Copy .env.example to .env and set credentials (both web logins and API tokens):
|
|
@@ -627,7 +914,7 @@ Next:
|
|
|
627
914
|
2) Run pre-authentication:
|
|
628
915
|
bun run xera:auth-setup
|
|
629
916
|
3) Start testing:
|
|
630
|
-
|
|
917
|
+
${editorLines}
|
|
631
918
|
` : `
|
|
632
919
|
Next:
|
|
633
920
|
1) Copy .env.example to .env and set your Jira credentials:
|
|
@@ -635,7 +922,7 @@ Next:
|
|
|
635
922
|
2) Run pre-authentication:
|
|
636
923
|
bun run xera:auth-setup
|
|
637
924
|
3) Start testing:
|
|
638
|
-
|
|
925
|
+
${editorLines}
|
|
639
926
|
`;
|
|
640
927
|
p.note(nextSteps.trim(), "Next steps");
|
|
641
928
|
p.outro(pc2.green("xera initialized!"));
|
|
@@ -644,24 +931,23 @@ Next:
|
|
|
644
931
|
// src/commands/init-update.ts
|
|
645
932
|
import {
|
|
646
933
|
copyFileSync,
|
|
647
|
-
existsSync as
|
|
648
|
-
mkdirSync as
|
|
934
|
+
existsSync as existsSync8,
|
|
935
|
+
mkdirSync as mkdirSync5,
|
|
649
936
|
readdirSync as readdirSync3,
|
|
650
|
-
readFileSync as
|
|
651
|
-
|
|
652
|
-
writeFileSync as writeFileSync3
|
|
937
|
+
readFileSync as readFileSync5,
|
|
938
|
+
writeFileSync as writeFileSync6
|
|
653
939
|
} from "fs";
|
|
654
940
|
import { createRequire as createRequire2 } from "module";
|
|
655
|
-
import {
|
|
941
|
+
import { join as join8 } from "path";
|
|
656
942
|
import * as p2 from "@clack/prompts";
|
|
657
943
|
import pc3 from "picocolors";
|
|
658
944
|
var require3 = createRequire2(import.meta.url);
|
|
659
945
|
var CLI_VERSION2 = require3("../package.json").version;
|
|
660
946
|
function detectAdaptersFromConfig(cwd) {
|
|
661
|
-
const configPath =
|
|
662
|
-
if (!
|
|
947
|
+
const configPath = join8(cwd, "xera.config.ts");
|
|
948
|
+
if (!existsSync8(configPath))
|
|
663
949
|
return null;
|
|
664
|
-
const cfg =
|
|
950
|
+
const cfg = readFileSync5(configPath, "utf8");
|
|
665
951
|
const m = cfg.match(/adapters:\s*\[([^\]]+)\]/);
|
|
666
952
|
if (!m)
|
|
667
953
|
return null;
|
|
@@ -712,7 +998,7 @@ ${roles.map((r) => ` ${r}: { envEmail: 'TEST_${r.toUpperCase().replace(/-
|
|
|
712
998
|
` web: {`,
|
|
713
999
|
` baseUrl: { staging: '${baseUrl}' },`,
|
|
714
1000
|
` defaultEnv: 'staging',`,
|
|
715
|
-
authBlock
|
|
1001
|
+
`${authBlock} },`
|
|
716
1002
|
].join(`
|
|
717
1003
|
`);
|
|
718
1004
|
}
|
|
@@ -738,12 +1024,12 @@ export const web = defineAuthSetup(async (page, _role, creds) => {
|
|
|
738
1024
|
async function initUpdateCommand(opts) {
|
|
739
1025
|
const cwd = process.cwd();
|
|
740
1026
|
p2.intro(pc3.cyan("xera init --update"));
|
|
741
|
-
const pkgPath =
|
|
742
|
-
if (!
|
|
1027
|
+
const pkgPath = join8(cwd, "package.json");
|
|
1028
|
+
if (!existsSync8(pkgPath)) {
|
|
743
1029
|
p2.cancel("No package.json found \u2014 run `xera init` first.");
|
|
744
1030
|
process.exit(1);
|
|
745
1031
|
}
|
|
746
|
-
const pkg = JSON.parse(
|
|
1032
|
+
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
747
1033
|
pkg.dependencies = pkg.dependencies ?? {};
|
|
748
1034
|
pkg.dependencies["@xera-ai/core"] = `^${CLI_VERSION2}`;
|
|
749
1035
|
pkg.dependencies["@xera-ai/prompts"] = `^${CLI_VERSION2}`;
|
|
@@ -763,79 +1049,47 @@ async function initUpdateCommand(opts) {
|
|
|
763
1049
|
pkg.scripts["xera:impact-prepare"] = "xera-internal impact-prepare";
|
|
764
1050
|
pkg.scripts["xera:heal-prepare"] = "xera-internal heal-prepare";
|
|
765
1051
|
pkg.scripts["xera:disputes"] = "xera-internal disputes";
|
|
766
|
-
|
|
767
|
-
const wfDir =
|
|
768
|
-
|
|
1052
|
+
writeFileSync6(pkgPath, JSON.stringify(pkg, null, 2));
|
|
1053
|
+
const wfDir = join8(cwd, ".github/workflows");
|
|
1054
|
+
mkdirSync5(wfDir, { recursive: true });
|
|
769
1055
|
try {
|
|
770
1056
|
const cliPkgPath = require3.resolve("@xera-ai/cli/package.json");
|
|
771
|
-
const cliTplPath =
|
|
772
|
-
copyFileSync(cliTplPath,
|
|
1057
|
+
const cliTplPath = join8(cliPkgPath, "..", "templates/xera-graph.yml.template");
|
|
1058
|
+
copyFileSync(cliTplPath, join8(wfDir, "xera-graph.yml"));
|
|
773
1059
|
p2.log.info("scaffolded .github/workflows/xera-graph.yml");
|
|
774
1060
|
} catch (_e) {
|
|
775
1061
|
p2.log.warn("skipped xera-graph.yml scaffold (re-run `xera init` to create it)");
|
|
776
1062
|
}
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
const
|
|
787
|
-
const
|
|
788
|
-
const
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
if (targets.every((s) => s.state === "missing")) {
|
|
808
|
-
for (const { path } of targets) {
|
|
809
|
-
mkdirSync3(dirname3(path), { recursive: true });
|
|
810
|
-
writeFileSync3(path, newContent);
|
|
811
|
-
}
|
|
812
|
-
p2.log.info(`+ ${name}`);
|
|
813
|
-
continue;
|
|
814
|
-
}
|
|
815
|
-
if (targets.every((s) => s.state === "same")) {
|
|
816
|
-
if (migratedLegacy)
|
|
817
|
-
p2.log.success(`migrated ${name} to .claude/skills/${base}/SKILL.md`);
|
|
818
|
-
else
|
|
819
|
-
p2.log.info(`= ${name}`);
|
|
820
|
-
continue;
|
|
821
|
-
}
|
|
822
|
-
const choice = await p2.select({
|
|
823
|
-
message: `${name} differs from package version`,
|
|
824
|
-
options: [
|
|
825
|
-
{ value: "keep", label: "Keep local" },
|
|
826
|
-
{ value: "overwrite", label: "Overwrite with package version" }
|
|
827
|
-
]
|
|
828
|
-
});
|
|
829
|
-
if (choice === "overwrite") {
|
|
830
|
-
for (const { path } of targets) {
|
|
831
|
-
mkdirSync3(dirname3(path), { recursive: true });
|
|
832
|
-
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);
|
|
833
1091
|
}
|
|
834
|
-
p2.log.
|
|
835
|
-
} else {
|
|
836
|
-
if (migratedLegacy)
|
|
837
|
-
p2.log.success(`migrated ${name} to .claude/skills/${base}/SKILL.md`);
|
|
838
|
-
p2.log.warn(`kept local ${name}`);
|
|
1092
|
+
p2.log.info(`refreshed ${base} across [${editorTargets.join(", ")}]`);
|
|
839
1093
|
}
|
|
840
1094
|
}
|
|
841
1095
|
const hasShapeFlags = opts.apiBaseUrl !== undefined || opts.openapiPath !== undefined || opts.authStrategy !== undefined || opts.httpRoles !== undefined || opts.stagingUrl !== undefined || opts.authEnabled !== undefined || opts.roles !== undefined;
|
|
@@ -933,7 +1187,7 @@ async function main() {
|
|
|
933
1187
|
cli.help();
|
|
934
1188
|
cli.version(VERSION);
|
|
935
1189
|
cli.usage("<command> [options]");
|
|
936
|
-
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) => {
|
|
937
1191
|
if (opts.update) {
|
|
938
1192
|
const updateOpts = { yes: !!opts.yes };
|
|
939
1193
|
if (opts.shape !== undefined) {
|
|
@@ -966,6 +1220,8 @@ async function main() {
|
|
|
966
1220
|
updateOpts.authEnabled = opts.authEnabled;
|
|
967
1221
|
if (opts.roles !== undefined)
|
|
968
1222
|
updateOpts.roles = opts.roles;
|
|
1223
|
+
if (opts.editor !== undefined)
|
|
1224
|
+
updateOpts.editor = opts.editor;
|
|
969
1225
|
await initUpdateCommand(updateOpts);
|
|
970
1226
|
return;
|
|
971
1227
|
}
|
|
@@ -1008,6 +1264,8 @@ async function main() {
|
|
|
1008
1264
|
initOpts.openapiPath = opts.openapiPath;
|
|
1009
1265
|
if (opts.httpRoles !== undefined)
|
|
1010
1266
|
initOpts.httpRoles = opts.httpRoles;
|
|
1267
|
+
if (opts.editor !== undefined)
|
|
1268
|
+
initOpts.editor = opts.editor;
|
|
1011
1269
|
await initCommand(initOpts);
|
|
1012
1270
|
});
|
|
1013
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.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"xera": "./bin/xera"
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"typecheck": "tsc --noEmit"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@xera-ai/core": "^0.
|
|
19
|
-
"@xera-ai/skills": "^0.
|
|
18
|
+
"@xera-ai/core": "^0.14.0",
|
|
19
|
+
"@xera-ai/skills": "^0.14.0",
|
|
20
20
|
"@clack/prompts": "1.4.0",
|
|
21
21
|
"cac": "7.0.0",
|
|
22
22
|
"picocolors": "1.1.1",
|