@xera-ai/core 0.1.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.
Files changed (80) hide show
  1. package/bin/internal.ts +4 -0
  2. package/dist/adapter/types.d.ts +70 -0
  3. package/dist/adapter/types.d.ts.map +1 -0
  4. package/dist/artifact/hash.d.ts +4 -0
  5. package/dist/artifact/hash.d.ts.map +1 -0
  6. package/dist/artifact/meta.d.ts +46 -0
  7. package/dist/artifact/meta.d.ts.map +1 -0
  8. package/dist/artifact/paths.d.ts +25 -0
  9. package/dist/artifact/paths.d.ts.map +1 -0
  10. package/dist/artifact/status.d.ts +96 -0
  11. package/dist/artifact/status.d.ts.map +1 -0
  12. package/dist/auth/encrypt.d.ts +4 -0
  13. package/dist/auth/encrypt.d.ts.map +1 -0
  14. package/dist/auth/key.d.ts +3 -0
  15. package/dist/auth/key.d.ts.map +1 -0
  16. package/dist/auth/refresh.d.ts +9 -0
  17. package/dist/auth/refresh.d.ts.map +1 -0
  18. package/dist/auth/state.d.ts +24 -0
  19. package/dist/auth/state.d.ts.map +1 -0
  20. package/dist/bin/internal.js +1013 -0
  21. package/dist/bin-internal/exec.d.ts +2 -0
  22. package/dist/bin-internal/exec.d.ts.map +1 -0
  23. package/dist/bin-internal/fetch.d.ts +5 -0
  24. package/dist/bin-internal/fetch.d.ts.map +1 -0
  25. package/dist/bin-internal/index.d.ts +2 -0
  26. package/dist/bin-internal/index.d.ts.map +1 -0
  27. package/dist/bin-internal/lint.d.ts +2 -0
  28. package/dist/bin-internal/lint.d.ts.map +1 -0
  29. package/dist/bin-internal/normalize.d.ts +2 -0
  30. package/dist/bin-internal/normalize.d.ts.map +1 -0
  31. package/dist/bin-internal/post.d.ts +2 -0
  32. package/dist/bin-internal/post.d.ts.map +1 -0
  33. package/dist/bin-internal/promote.d.ts +2 -0
  34. package/dist/bin-internal/promote.d.ts.map +1 -0
  35. package/dist/bin-internal/report.d.ts +2 -0
  36. package/dist/bin-internal/report.d.ts.map +1 -0
  37. package/dist/bin-internal/status-cmd.d.ts +2 -0
  38. package/dist/bin-internal/status-cmd.d.ts.map +1 -0
  39. package/dist/bin-internal/typecheck.d.ts +2 -0
  40. package/dist/bin-internal/typecheck.d.ts.map +1 -0
  41. package/dist/bin-internal/unlock.d.ts +2 -0
  42. package/dist/bin-internal/unlock.d.ts.map +1 -0
  43. package/dist/bin-internal/validate-feature.d.ts +2 -0
  44. package/dist/bin-internal/validate-feature.d.ts.map +1 -0
  45. package/dist/classifier/aggregate.d.ts +3 -0
  46. package/dist/classifier/aggregate.d.ts.map +1 -0
  47. package/dist/classifier/history.d.ts +13 -0
  48. package/dist/classifier/history.d.ts.map +1 -0
  49. package/dist/classifier/types.d.ts +26 -0
  50. package/dist/classifier/types.d.ts.map +1 -0
  51. package/dist/config/define.d.ts +3 -0
  52. package/dist/config/define.d.ts.map +1 -0
  53. package/dist/config/load.d.ts +3 -0
  54. package/dist/config/load.d.ts.map +1 -0
  55. package/dist/config/schema.d.ts +326 -0
  56. package/dist/config/schema.d.ts.map +1 -0
  57. package/dist/index.d.ts +20 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/jira/client.d.ts +11 -0
  60. package/dist/jira/client.d.ts.map +1 -0
  61. package/dist/jira/fields.d.ts +7 -0
  62. package/dist/jira/fields.d.ts.map +1 -0
  63. package/dist/jira/mcp-backend.d.ts +3 -0
  64. package/dist/jira/mcp-backend.d.ts.map +1 -0
  65. package/dist/jira/rest-backend.d.ts +8 -0
  66. package/dist/jira/rest-backend.d.ts.map +1 -0
  67. package/dist/jira/retry.d.ts +8 -0
  68. package/dist/jira/retry.d.ts.map +1 -0
  69. package/dist/jira/types.d.ts +29 -0
  70. package/dist/jira/types.d.ts.map +1 -0
  71. package/dist/lock/file-lock.d.ts +12 -0
  72. package/dist/lock/file-lock.d.ts.map +1 -0
  73. package/dist/logging/ndjson-logger.d.ts +11 -0
  74. package/dist/logging/ndjson-logger.d.ts.map +1 -0
  75. package/dist/reporter/jira-comment.d.ts +9 -0
  76. package/dist/reporter/jira-comment.d.ts.map +1 -0
  77. package/dist/reporter/status-writer.d.ts +14 -0
  78. package/dist/reporter/status-writer.d.ts.map +1 -0
  79. package/dist/src/index.js +587 -0
  80. package/package.json +30 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-lock.d.ts","sourceRoot":"","sources":["../../src/lock/file-lock.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAgBhE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAE9C;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAGtD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAcjD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAE9C"}
@@ -0,0 +1,11 @@
1
+ export interface LogEntry {
2
+ ts: string;
3
+ [key: string]: unknown;
4
+ }
5
+ export declare class NdjsonLogger {
6
+ private readonly path;
7
+ constructor(path: string);
8
+ log(payload: Record<string, unknown>): void;
9
+ static readAll(path: string): LogEntry[];
10
+ }
11
+ //# sourceMappingURL=ndjson-logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ndjson-logger.d.ts","sourceRoot":"","sources":["../../src/logging/ndjson-logger.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,qBAAa,YAAY;IACX,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,MAAM;IAIzC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAK3C,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,EAAE;CAMzC"}
@@ -0,0 +1,9 @@
1
+ import type { ClassifyOutput } from '../classifier/types';
2
+ export interface JiraCommentInput extends ClassifyOutput {
3
+ ticket: string;
4
+ runId: string;
5
+ xeraVersion: string;
6
+ promptsVersion: string;
7
+ }
8
+ export declare function buildJiraComment(input: JiraCommentInput): string;
9
+ //# sourceMappingURL=jira-comment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jira-comment.d.ts","sourceRoot":"","sources":["../../src/reporter/jira-comment.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,MAAM,WAAW,gBAAiB,SAAQ,cAAc;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAqBhE"}
@@ -0,0 +1,14 @@
1
+ import type { ClassifyOutput } from '../classifier/types';
2
+ export interface StatusWriterInput {
3
+ ticket: string;
4
+ runTs: string;
5
+ classification: ClassifyOutput;
6
+ scenarioCounts: {
7
+ total: number;
8
+ passed: number;
9
+ failed: number;
10
+ skipped: number;
11
+ };
12
+ }
13
+ export declare function writeStatusFromClassification(path: string, input: StatusWriterInput): void;
14
+ //# sourceMappingURL=status-writer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status-writer.d.ts","sourceRoot":"","sources":["../../src/reporter/status-writer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAI1D,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,cAAc,CAAC;IAC/B,cAAc,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CACpF;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAyB1F"}
@@ -0,0 +1,587 @@
1
+ // @bun
2
+ // src/config/schema.ts
3
+ import { z } from "zod";
4
+ var AuthRoleSchema = z.object({
5
+ envEmail: z.string().min(1),
6
+ envPassword: z.string().min(1)
7
+ });
8
+ var AuthSchema = z.object({
9
+ strategy: z.enum(["storageState", "apiToken", "none"]).default("none"),
10
+ ttl: z.string().default("8h"),
11
+ refreshBuffer: z.string().default("30m"),
12
+ setupScript: z.string().optional(),
13
+ roles: z.record(z.string(), AuthRoleSchema).default({})
14
+ });
15
+ var WebSchema = z.object({
16
+ baseUrl: z.record(z.string(), z.string().url()).refine((m) => Object.keys(m).length > 0, {
17
+ message: "baseUrl must have at least one environment"
18
+ }),
19
+ defaultEnv: z.string(),
20
+ auth: AuthSchema.default({}),
21
+ testData: z.object({
22
+ users: z.record(z.string(), z.object({ fromAuth: z.string() })).default({})
23
+ }).default({ users: {} })
24
+ }).refine((w) => w.baseUrl[w.defaultEnv] !== undefined, {
25
+ message: "defaultEnv must exist in baseUrl map",
26
+ path: ["defaultEnv"]
27
+ });
28
+ var JiraSchema = z.object({
29
+ baseUrl: z.string().url(),
30
+ projectKeys: z.array(z.string().min(1)).min(1),
31
+ fields: z.object({
32
+ story: z.string().min(1),
33
+ acceptanceCriteria: z.string().optional(),
34
+ attachments: z.string().default("attachment")
35
+ })
36
+ });
37
+ var AISchema = z.object({
38
+ livePageSnapshot: z.boolean().default(true),
39
+ confidenceThreshold: z.enum(["low", "medium", "high"]).default("medium"),
40
+ maxRetries: z.object({
41
+ typecheck: z.number().int().min(0).max(5).default(2),
42
+ lint: z.number().int().min(0).max(5).default(2),
43
+ validateFeature: z.number().int().min(0).max(5).default(2)
44
+ }).default({})
45
+ }).default({});
46
+ var ReportingSchema = z.object({
47
+ language: z.enum(["en", "vi"]).default("en"),
48
+ postToJira: z.boolean().default(true),
49
+ transition: z.object({
50
+ onPass: z.string().nullable().default(null),
51
+ onFail: z.string().nullable().default(null)
52
+ }).default({}),
53
+ artifactLinks: z.enum(["git", "local"]).default("git")
54
+ }).default({});
55
+ var XeraConfigSchema = z.object({
56
+ jira: JiraSchema,
57
+ web: WebSchema,
58
+ ai: AISchema,
59
+ reporting: ReportingSchema,
60
+ adapters: z.array(z.string().min(1)).min(1).default(["web"])
61
+ });
62
+ // src/config/define.ts
63
+ function defineConfig(config) {
64
+ return config;
65
+ }
66
+ // src/config/load.ts
67
+ import { existsSync } from "fs";
68
+ import { join } from "path";
69
+ import { pathToFileURL } from "url";
70
+ async function loadConfig(cwd) {
71
+ const path = join(cwd, "xera.config.ts");
72
+ if (!existsSync(path)) {
73
+ throw new Error(`xera.config.ts not found in ${cwd}`);
74
+ }
75
+ const mod = await import(pathToFileURL(path).href);
76
+ const raw = mod.default ?? mod;
77
+ return XeraConfigSchema.parse(raw);
78
+ }
79
+ // src/artifact/paths.ts
80
+ import { join as join2 } from "path";
81
+ var TICKET_RE = /^[A-Z][A-Z0-9_]*-\d+$|^SAMPLE-\d+$/;
82
+ function resolveArtifactPaths(repoRoot, ticket) {
83
+ if (!TICKET_RE.test(ticket)) {
84
+ throw new Error(`Invalid ticket key: "${ticket}" (expected e.g. JIRA-123 or SAMPLE-001)`);
85
+ }
86
+ const ticketDir = join2(repoRoot, ".xera", ticket);
87
+ return {
88
+ ticketDir,
89
+ storyPath: join2(ticketDir, "story.md"),
90
+ featurePath: join2(ticketDir, "test.feature"),
91
+ specPath: join2(ticketDir, "spec.ts"),
92
+ pageObjectsDir: join2(ticketDir, "page-objects"),
93
+ runsDir: join2(ticketDir, "runs"),
94
+ metaPath: join2(ticketDir, "meta.json"),
95
+ statusPath: join2(ticketDir, "status.json"),
96
+ logPath: join2(ticketDir, "xera.log"),
97
+ lockPath: join2(ticketDir, ".lock"),
98
+ authDir: join2(repoRoot, ".xera", ".auth"),
99
+ runPath: (runId) => {
100
+ const runDir = join2(ticketDir, "runs", runId);
101
+ return {
102
+ runDir,
103
+ reportJsonPath: join2(runDir, "report.json"),
104
+ tracePath: join2(runDir, "trace.zip"),
105
+ normalizedPath: join2(runDir, "normalized.json"),
106
+ screenshotsDir: join2(runDir, "screenshots"),
107
+ videoDir: join2(runDir, "videos")
108
+ };
109
+ }
110
+ };
111
+ }
112
+ function generateRunId(now = new Date) {
113
+ return now.toISOString().replace(/[:.]/g, "-").replace("Z", "");
114
+ }
115
+ // src/artifact/hash.ts
116
+ import { createHash } from "crypto";
117
+ import { existsSync as existsSync2, readFileSync } from "fs";
118
+ function hashString(s) {
119
+ return `sha256:${createHash("sha256").update(s).digest("hex")}`;
120
+ }
121
+ function hashFile(path) {
122
+ return hashString(readFileSync(path, "utf8"));
123
+ }
124
+ function hashFileIfExists(path) {
125
+ if (!existsSync2(path))
126
+ return null;
127
+ return hashFile(path);
128
+ }
129
+ // src/artifact/meta.ts
130
+ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
131
+ import { dirname } from "path";
132
+ import { z as z2 } from "zod";
133
+ var MetaJsonSchema = z2.object({
134
+ ticket: z2.string(),
135
+ adapter: z2.string(),
136
+ xera_version: z2.string(),
137
+ prompts_version: z2.string(),
138
+ fetched_at: z2.string().optional(),
139
+ story_hash: z2.string().optional(),
140
+ feature_generated_at: z2.string().optional(),
141
+ feature_generated_from_story_hash: z2.string().optional(),
142
+ feature_hash: z2.string().optional(),
143
+ script_generated_at: z2.string().optional(),
144
+ script_generated_from_feature_hash: z2.string().optional(),
145
+ script_warnings: z2.array(z2.string()).optional()
146
+ });
147
+ function readMeta(path) {
148
+ if (!existsSync3(path))
149
+ return null;
150
+ return MetaJsonSchema.parse(JSON.parse(readFileSync2(path, "utf8")));
151
+ }
152
+ function writeMeta(path, meta) {
153
+ mkdirSync(dirname(path), { recursive: true });
154
+ writeFileSync(path, JSON.stringify(meta, null, 2));
155
+ }
156
+ function updateMeta(path, patch) {
157
+ const existing = readMeta(path);
158
+ if (!existing) {
159
+ throw new Error(`meta.json not found at ${path}; cannot update`);
160
+ }
161
+ const next = { ...existing, ...patch };
162
+ writeMeta(path, next);
163
+ return next;
164
+ }
165
+ // src/artifact/status.ts
166
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
167
+ import { dirname as dirname2 } from "path";
168
+ import { z as z3 } from "zod";
169
+ var ClassificationEnum = z3.enum(["PASS", "REAL_BUG", "SELECTOR_DRIFT", "FLAKY", "TEST_BUG"]);
170
+ var ResultEnum = z3.enum(["PASS", "FAIL"]);
171
+ var ConfidenceEnum = z3.enum(["low", "medium", "high"]);
172
+ var HistoryEntrySchema = z3.object({
173
+ ts: z3.string(),
174
+ result: ResultEnum,
175
+ class: ClassificationEnum
176
+ });
177
+ var StatusJsonSchema = z3.object({
178
+ ticket: z3.string(),
179
+ lastRun: z3.string(),
180
+ result: ResultEnum,
181
+ classification: ClassificationEnum,
182
+ confidence: ConfidenceEnum,
183
+ scenarios: z3.object({
184
+ total: z3.number().int().nonnegative(),
185
+ passed: z3.number().int().nonnegative(),
186
+ failed: z3.number().int().nonnegative(),
187
+ skipped: z3.number().int().nonnegative()
188
+ }),
189
+ history: z3.array(HistoryEntrySchema).default([]),
190
+ last_jira_comment_id: z3.string().optional()
191
+ });
192
+ var HISTORY_CAP = 20;
193
+ function readStatus(path) {
194
+ if (!existsSync4(path))
195
+ return null;
196
+ return StatusJsonSchema.parse(JSON.parse(readFileSync3(path, "utf8")));
197
+ }
198
+ function writeStatus(path, status) {
199
+ mkdirSync2(dirname2(path), { recursive: true });
200
+ writeFileSync2(path, JSON.stringify(status, null, 2));
201
+ }
202
+ function appendHistory(path, entry) {
203
+ const s = readStatus(path);
204
+ if (!s) {
205
+ throw new Error(`status.json not found at ${path}`);
206
+ }
207
+ s.history = [entry, ...s.history].slice(0, HISTORY_CAP);
208
+ writeStatus(path, s);
209
+ return s;
210
+ }
211
+ // src/logging/ndjson-logger.ts
212
+ import { appendFileSync, mkdirSync as mkdirSync3, existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
213
+ import { dirname as dirname3 } from "path";
214
+
215
+ class NdjsonLogger {
216
+ path;
217
+ constructor(path) {
218
+ this.path = path;
219
+ mkdirSync3(dirname3(path), { recursive: true });
220
+ }
221
+ log(payload) {
222
+ const entry = { ts: new Date().toISOString(), ...payload };
223
+ appendFileSync(this.path, `${JSON.stringify(entry)}
224
+ `);
225
+ }
226
+ static readAll(path) {
227
+ if (!existsSync5(path))
228
+ return [];
229
+ const txt = readFileSync4(path, "utf8").trim();
230
+ if (!txt)
231
+ return [];
232
+ return txt.split(`
233
+ `).map((line) => JSON.parse(line));
234
+ }
235
+ }
236
+ // src/lock/file-lock.ts
237
+ import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync3, unlinkSync, mkdirSync as mkdirSync4 } from "fs";
238
+ import { dirname as dirname4 } from "path";
239
+ import { hostname } from "os";
240
+ function acquireLock(path, runId) {
241
+ if (existsSync6(path))
242
+ return false;
243
+ mkdirSync4(dirname4(path), { recursive: true });
244
+ const data = {
245
+ pid: process.pid,
246
+ hostname: hostname(),
247
+ started_at: new Date().toISOString(),
248
+ run_id: runId
249
+ };
250
+ try {
251
+ writeFileSync3(path, JSON.stringify(data), { flag: "wx" });
252
+ return true;
253
+ } catch {
254
+ return false;
255
+ }
256
+ }
257
+ function releaseLock(path) {
258
+ if (existsSync6(path))
259
+ unlinkSync(path);
260
+ }
261
+ function readLock(path) {
262
+ if (!existsSync6(path))
263
+ return null;
264
+ return JSON.parse(readFileSync5(path, "utf8"));
265
+ }
266
+ function isLockStale(path) {
267
+ const lock = readLock(path);
268
+ if (!lock)
269
+ return true;
270
+ if (lock.hostname !== hostname()) {
271
+ return false;
272
+ }
273
+ try {
274
+ process.kill(lock.pid, 0);
275
+ return false;
276
+ } catch {
277
+ return true;
278
+ }
279
+ }
280
+ function forceUnlock(path) {
281
+ releaseLock(path);
282
+ }
283
+ // src/jira/mcp-backend.ts
284
+ import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5 } from "fs";
285
+ import { tmpdir } from "os";
286
+ import { join as join3 } from "path";
287
+ var MCP_ENV = "XERA_MCP_JIRA";
288
+ async function createMcpBackend(_baseUrl) {
289
+ if (process.env[MCP_ENV] !== "1")
290
+ return null;
291
+ const tmpDir = join3(tmpdir(), "xera-mcp");
292
+ mkdirSync5(tmpDir, { recursive: true });
293
+ return {
294
+ backend: "mcp",
295
+ async fetchTicket(key, _fields) {
296
+ const cachePath = join3(tmpDir, `${key}.json`);
297
+ if (!existsSync7(cachePath)) {
298
+ throw new Error(`MCP-mode fetch requires the skill to first call mcp__atlassian__getJiraIssue and write ${cachePath}. ` + `If you are running this directly, unset ${MCP_ENV} to use REST.`);
299
+ }
300
+ const parsed = JSON.parse(readFileSync6(cachePath, "utf8"));
301
+ return parsed;
302
+ },
303
+ async postComment(key, body) {
304
+ const outPath = join3(tmpDir, `${key}.comment.json`);
305
+ writeFileSync4(outPath, JSON.stringify({ key, body }));
306
+ return { id: "mcp-pending" };
307
+ },
308
+ async transitionStatus(key, statusName) {
309
+ const outPath = join3(tmpDir, `${key}.transition.json`);
310
+ writeFileSync4(outPath, JSON.stringify({ key, statusName }));
311
+ },
312
+ async listFields(_sampleKey) {
313
+ throw new Error("listFields is REST-only; init flow uses REST for field discovery.");
314
+ }
315
+ };
316
+ }
317
+
318
+ // src/jira/rest-backend.ts
319
+ function createRestBackend(baseUrl, creds) {
320
+ const authHeader = `Basic ${Buffer.from(`${creds.email}:${creds.apiToken}`).toString("base64")}`;
321
+ const base = baseUrl.replace(/\/$/, "");
322
+ async function req(path, init) {
323
+ const r = await fetch(`${base}${path}`, {
324
+ ...init,
325
+ headers: {
326
+ Authorization: authHeader,
327
+ Accept: "application/json",
328
+ "Content-Type": "application/json",
329
+ ...init?.headers ?? {}
330
+ }
331
+ });
332
+ if (!r.ok && r.status !== 201) {
333
+ throw new Error(`Jira REST ${init?.method ?? "GET"} ${path} failed: ${r.status} ${await r.text()}`);
334
+ }
335
+ return r;
336
+ }
337
+ return {
338
+ backend: "rest",
339
+ async fetchTicket(key, fields) {
340
+ const want = ["summary", fields.story];
341
+ if (fields.acceptanceCriteria)
342
+ want.push(fields.acceptanceCriteria);
343
+ want.push("attachment");
344
+ const r = await req(`/rest/api/3/issue/${encodeURIComponent(key)}?fields=${want.join(",")}`);
345
+ const json = await r.json();
346
+ const f = json.fields;
347
+ const attachments = Array.isArray(f.attachment) ? f.attachment.map((a) => ({ filename: a.filename, url: a.content })) : [];
348
+ const ticket = {
349
+ key: json.key,
350
+ summary: String(f.summary ?? ""),
351
+ story: String(f[fields.story] ?? ""),
352
+ attachments,
353
+ raw: f
354
+ };
355
+ if (fields.acceptanceCriteria) {
356
+ ticket.acceptanceCriteria = String(f[fields.acceptanceCriteria] ?? "");
357
+ }
358
+ return ticket;
359
+ },
360
+ async postComment(key, body) {
361
+ const r = await req(`/rest/api/3/issue/${encodeURIComponent(key)}/comment`, {
362
+ method: "POST",
363
+ body: JSON.stringify({
364
+ body: { type: "doc", version: 1, content: [{ type: "paragraph", content: [{ type: "text", text: body }] }] }
365
+ })
366
+ });
367
+ const json = await r.json();
368
+ return { id: json.id };
369
+ },
370
+ async transitionStatus(key, statusName) {
371
+ const tr = await req(`/rest/api/3/issue/${encodeURIComponent(key)}/transitions`);
372
+ const json = await tr.json();
373
+ const t = json.transitions.find((x) => x.name === statusName);
374
+ if (!t)
375
+ throw new Error(`No transition named "${statusName}" available for ${key}`);
376
+ await req(`/rest/api/3/issue/${encodeURIComponent(key)}/transitions`, {
377
+ method: "POST",
378
+ body: JSON.stringify({ transition: { id: t.id } })
379
+ });
380
+ },
381
+ async listFields(sampleKey) {
382
+ const r = await req(`/rest/api/3/issue/${encodeURIComponent(sampleKey)}?fields=*all`);
383
+ const json = await r.json();
384
+ return Object.entries(json.fields).map(([id, value]) => ({
385
+ id,
386
+ name: id,
387
+ hasContent: value !== null && value !== undefined && value !== ""
388
+ }));
389
+ }
390
+ };
391
+ }
392
+
393
+ // src/jira/client.ts
394
+ async function createJiraClient(opts) {
395
+ if (opts.preferMcp !== false) {
396
+ const mcp = await createMcpBackend(opts.baseUrl);
397
+ if (mcp)
398
+ return mcp;
399
+ }
400
+ if (!opts.rest) {
401
+ throw new Error("Atlassian MCP not connected and no REST credentials provided (JIRA_EMAIL + JIRA_API_TOKEN).");
402
+ }
403
+ return createRestBackend(opts.baseUrl, opts.rest);
404
+ }
405
+ // src/jira/fields.ts
406
+ var PREFERRED_STORY_IDS = ["description", "story"];
407
+ function rankStoryCandidates(fields) {
408
+ return fields.filter((f) => f.hasContent).filter((f) => !["attachment", "comment", "created", "updated", "reporter", "creator"].includes(f.id)).sort((a, b) => {
409
+ const ai = PREFERRED_STORY_IDS.indexOf(a.id);
410
+ const bi = PREFERRED_STORY_IDS.indexOf(b.id);
411
+ if (ai >= 0 && bi >= 0)
412
+ return ai - bi;
413
+ if (ai >= 0)
414
+ return -1;
415
+ if (bi >= 0)
416
+ return 1;
417
+ return a.id.localeCompare(b.id);
418
+ });
419
+ }
420
+ // src/jira/retry.ts
421
+ async function withRetry(fn, opts) {
422
+ let attempt = 0;
423
+ let lastErr;
424
+ while (attempt < opts.maxAttempts) {
425
+ try {
426
+ return await fn();
427
+ } catch (err) {
428
+ lastErr = err;
429
+ if (opts.shouldRetry && !opts.shouldRetry(err))
430
+ throw err;
431
+ attempt++;
432
+ if (attempt >= opts.maxAttempts)
433
+ break;
434
+ const delay = opts.baseMs * Math.pow(opts.factor, attempt - 1);
435
+ await new Promise((r) => setTimeout(r, delay));
436
+ }
437
+ }
438
+ throw lastErr;
439
+ }
440
+ // src/auth/encrypt.ts
441
+ import { randomBytes, createCipheriv, createDecipheriv } from "crypto";
442
+ var ALGO = "aes-256-gcm";
443
+ var KEY_LEN = 32;
444
+ var IV_LEN = 12;
445
+ var TAG_LEN = 16;
446
+ var VERSION = "v1";
447
+ function generateKey() {
448
+ return randomBytes(KEY_LEN).toString("hex");
449
+ }
450
+ function keyToBuf(key) {
451
+ const buf = Buffer.from(key, "hex");
452
+ if (buf.length !== KEY_LEN)
453
+ throw new Error(`Key must be ${KEY_LEN} bytes (got ${buf.length})`);
454
+ return buf;
455
+ }
456
+ function encrypt(plaintext, keyHex) {
457
+ const key = keyToBuf(keyHex);
458
+ const iv = randomBytes(IV_LEN);
459
+ const cipher = createCipheriv(ALGO, key, iv);
460
+ const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
461
+ const tag = cipher.getAuthTag();
462
+ return `${VERSION}:${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
463
+ }
464
+ function decrypt(ciphertext, keyHex) {
465
+ const [version, ivB64, tagB64, ctB64] = ciphertext.split(":");
466
+ if (version !== VERSION)
467
+ throw new Error(`Unsupported ciphertext version: ${version}`);
468
+ if (!ivB64 || !tagB64 || !ctB64)
469
+ throw new Error("Malformed ciphertext");
470
+ const key = keyToBuf(keyHex);
471
+ const iv = Buffer.from(ivB64, "base64");
472
+ const tag = Buffer.from(tagB64, "base64");
473
+ const ct = Buffer.from(ctB64, "base64");
474
+ if (tag.length !== TAG_LEN)
475
+ throw new Error("Bad auth tag length");
476
+ const decipher = createDecipheriv(ALGO, key, iv);
477
+ decipher.setAuthTag(tag);
478
+ return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8");
479
+ }
480
+ // src/auth/key.ts
481
+ var AUTH_KEY_ENV = "XERA_AUTH_KEY";
482
+ function resolveAuthKey() {
483
+ const key = process.env[AUTH_KEY_ENV];
484
+ if (!key) {
485
+ throw new Error(`${AUTH_KEY_ENV} not set. It is auto-generated by \`xera init\` and saved to .env. ` + `If you deleted .env, regenerate it by running \`xera init --update\` \u2014 note that any cached auth state will be invalidated.`);
486
+ }
487
+ if (!/^[0-9a-f]{64}$/i.test(key)) {
488
+ throw new Error(`${AUTH_KEY_ENV} must be a 64-character hex string (32 bytes).`);
489
+ }
490
+ return key;
491
+ }
492
+ // src/auth/state.ts
493
+ import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
494
+ import { join as join4 } from "path";
495
+ import { z as z4 } from "zod";
496
+ var AuthStateEntrySchema = z4.object({
497
+ role: z4.string(),
498
+ strategy: z4.enum(["storageState", "apiToken"]),
499
+ created_at: z4.string(),
500
+ expires_at: z4.string(),
501
+ payload: z4.record(z4.string(), z4.unknown())
502
+ });
503
+ function pathFor(authDir, role) {
504
+ return join4(authDir, `${role}.json`);
505
+ }
506
+ function writeAuthState(authDir, entry) {
507
+ mkdirSync6(authDir, { recursive: true });
508
+ const ct = encrypt(JSON.stringify(entry), resolveAuthKey());
509
+ writeFileSync5(pathFor(authDir, entry.role), ct);
510
+ }
511
+ function readAuthState(authDir, role) {
512
+ const p = pathFor(authDir, role);
513
+ if (!existsSync8(p))
514
+ return null;
515
+ const txt = readFileSync7(p, "utf8");
516
+ const plain = decrypt(txt, resolveAuthKey());
517
+ return AuthStateEntrySchema.parse(JSON.parse(plain));
518
+ }
519
+ // src/auth/refresh.ts
520
+ var RE = /^(\d+)([hms])$/;
521
+ function parseDuration(d) {
522
+ const m = RE.exec(d);
523
+ if (!m)
524
+ throw new Error(`Bad duration "${d}" \u2014 expected e.g. "8h", "30m", "45s"`);
525
+ const n = Number(m[1]);
526
+ const unit = m[2];
527
+ if (unit === "h")
528
+ return n * 3600 * 1000;
529
+ if (unit === "m")
530
+ return n * 60 * 1000;
531
+ return n * 1000;
532
+ }
533
+ function needsRefresh(entry, policy, now = new Date) {
534
+ if (!entry)
535
+ return true;
536
+ const ttlMs = parseDuration(policy.ttl);
537
+ const bufMs = parseDuration(policy.refreshBuffer);
538
+ const createdAt = new Date(entry.created_at).getTime();
539
+ if (now.getTime() - createdAt > ttlMs)
540
+ return true;
541
+ const expiresAt = new Date(entry.expires_at).getTime();
542
+ if (expiresAt - now.getTime() < bufMs)
543
+ return true;
544
+ return false;
545
+ }
546
+
547
+ // src/index.ts
548
+ var VERSION2 = "0.1.0";
549
+ export {
550
+ writeStatus,
551
+ writeMeta,
552
+ writeAuthState,
553
+ withRetry,
554
+ updateMeta,
555
+ resolveAuthKey,
556
+ resolveArtifactPaths,
557
+ releaseLock,
558
+ readStatus,
559
+ readMeta,
560
+ readLock,
561
+ readAuthState,
562
+ rankStoryCandidates,
563
+ parseDuration,
564
+ needsRefresh,
565
+ loadConfig,
566
+ isLockStale,
567
+ hashString,
568
+ hashFileIfExists,
569
+ hashFile,
570
+ generateRunId,
571
+ generateKey,
572
+ forceUnlock,
573
+ encrypt,
574
+ defineConfig,
575
+ decrypt,
576
+ createJiraClient,
577
+ appendHistory,
578
+ acquireLock,
579
+ XeraConfigSchema,
580
+ VERSION2 as VERSION,
581
+ StatusJsonSchema,
582
+ NdjsonLogger,
583
+ MetaJsonSchema,
584
+ HistoryEntrySchema,
585
+ AuthStateEntrySchema,
586
+ AUTH_KEY_ENV
587
+ };
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@xera-ai/core",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "bun": "./src/index.ts",
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./adapter": {
14
+ "bun": "./src/adapter/types.ts",
15
+ "import": "./dist/adapter/types.js",
16
+ "types": "./dist/adapter/types.d.ts"
17
+ }
18
+ },
19
+ "bin": { "xera-internal": "./dist/bin/internal.js" },
20
+ "files": ["dist", "bin"],
21
+ "scripts": {
22
+ "build": "bun build ./src/index.ts ./bin/internal.ts --outdir ./dist --target bun --external @playwright/test --external @xera-ai/web --external zod",
23
+ "typecheck": "tsc --noEmit"
24
+ },
25
+ "dependencies": {
26
+ "zod": "3.23.8",
27
+ "@xera-ai/web": "workspace:*",
28
+ "@playwright/test": "1.48.0"
29
+ }
30
+ }