@xera-ai/core 0.1.7 → 0.4.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 (131) hide show
  1. package/bin/internal.ts +1 -0
  2. package/dist/adapter/types.d.ts +1 -1
  3. package/dist/adapter/types.d.ts.map +1 -1
  4. package/dist/artifact/meta.d.ts +2 -28
  5. package/dist/artifact/meta.d.ts.map +1 -1
  6. package/dist/artifact/status.d.ts +49 -74
  7. package/dist/artifact/status.d.ts.map +1 -1
  8. package/dist/auth/key.d.ts.map +1 -1
  9. package/dist/auth/refresh.d.ts.map +1 -1
  10. package/dist/auth/state.d.ts +5 -14
  11. package/dist/auth/state.d.ts.map +1 -1
  12. package/dist/bin/internal.js +10037 -746
  13. package/dist/bin-internal/doctor.d.ts +5 -0
  14. package/dist/bin-internal/doctor.d.ts.map +1 -0
  15. package/dist/bin-internal/eval-deterministic.d.ts +5 -0
  16. package/dist/bin-internal/eval-deterministic.d.ts.map +1 -0
  17. package/dist/bin-internal/eval-prepare.d.ts +7 -0
  18. package/dist/bin-internal/eval-prepare.d.ts.map +1 -0
  19. package/dist/bin-internal/eval-report.d.ts +5 -0
  20. package/dist/bin-internal/eval-report.d.ts.map +1 -0
  21. package/dist/bin-internal/exec.d.ts.map +1 -1
  22. package/dist/bin-internal/fetch.d.ts.map +1 -1
  23. package/dist/bin-internal/graph-backfill.d.ts +2 -0
  24. package/dist/bin-internal/graph-backfill.d.ts.map +1 -0
  25. package/dist/bin-internal/graph-query.d.ts +2 -0
  26. package/dist/bin-internal/graph-query.d.ts.map +1 -0
  27. package/dist/bin-internal/graph-record-script.d.ts +2 -0
  28. package/dist/bin-internal/graph-record-script.d.ts.map +1 -0
  29. package/dist/bin-internal/graph-record.d.ts +3 -0
  30. package/dist/bin-internal/graph-record.d.ts.map +1 -0
  31. package/dist/bin-internal/graph-snapshot.d.ts +2 -0
  32. package/dist/bin-internal/graph-snapshot.d.ts.map +1 -0
  33. package/dist/bin-internal/heal-prepare.d.ts +19 -0
  34. package/dist/bin-internal/heal-prepare.d.ts.map +1 -0
  35. package/dist/bin-internal/index.d.ts.map +1 -1
  36. package/dist/bin-internal/lint.d.ts.map +1 -1
  37. package/dist/bin-internal/normalize.d.ts.map +1 -1
  38. package/dist/bin-internal/post.d.ts.map +1 -1
  39. package/dist/bin-internal/status-cmd.d.ts.map +1 -1
  40. package/dist/bin-internal/typecheck.d.ts.map +1 -1
  41. package/dist/bin-internal/unlock.d.ts.map +1 -1
  42. package/dist/bin-internal/validate-feature.d.ts.map +1 -1
  43. package/dist/bin-internal/verify-prompts.d.ts +7 -0
  44. package/dist/bin-internal/verify-prompts.d.ts.map +1 -0
  45. package/dist/classifier/aggregate.d.ts.map +1 -1
  46. package/dist/config/define.d.ts.map +1 -1
  47. package/dist/config/load.d.ts.map +1 -1
  48. package/dist/config/schema.d.ts +38 -298
  49. package/dist/config/schema.d.ts.map +1 -1
  50. package/dist/eval/paths.d.ts +15 -0
  51. package/dist/eval/paths.d.ts.map +1 -0
  52. package/dist/eval/run-id.d.ts +6 -0
  53. package/dist/eval/run-id.d.ts.map +1 -0
  54. package/dist/eval/types.d.ts +203 -0
  55. package/dist/eval/types.d.ts.map +1 -0
  56. package/dist/graph/cost.d.ts +21 -0
  57. package/dist/graph/cost.d.ts.map +1 -0
  58. package/dist/graph/index.d.ts +8 -0
  59. package/dist/graph/index.d.ts.map +1 -0
  60. package/dist/graph/paths.d.ts +10 -0
  61. package/dist/graph/paths.d.ts.map +1 -0
  62. package/dist/graph/schema.d.ts +177 -0
  63. package/dist/graph/schema.d.ts.map +1 -0
  64. package/dist/graph/store.d.ts +14 -0
  65. package/dist/graph/store.d.ts.map +1 -0
  66. package/dist/graph/types.d.ts +151 -0
  67. package/dist/graph/types.d.ts.map +1 -0
  68. package/dist/graph/ulid.d.ts +2 -0
  69. package/dist/graph/ulid.d.ts.map +1 -0
  70. package/dist/index.d.ts +11 -11
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/jira/client.d.ts.map +1 -1
  73. package/dist/jira/fields.d.ts.map +1 -1
  74. package/dist/jira/rest-backend.d.ts.map +1 -1
  75. package/dist/reporter/jira-comment.d.ts.map +1 -1
  76. package/dist/reporter/status-writer.d.ts.map +1 -1
  77. package/dist/src/index.js +349 -321
  78. package/package.json +19 -13
  79. package/src/adapter/types.ts +5 -2
  80. package/src/artifact/meta.ts +1 -1
  81. package/src/artifact/status.ts +1 -1
  82. package/src/auth/encrypt.ts +2 -2
  83. package/src/auth/key.ts +1 -2
  84. package/src/auth/refresh.ts +5 -1
  85. package/src/auth/state.ts +2 -2
  86. package/src/bin-internal/doctor.ts +169 -0
  87. package/src/bin-internal/eval-deterministic.ts +149 -0
  88. package/src/bin-internal/eval-prepare.ts +214 -0
  89. package/src/bin-internal/eval-report.ts +177 -0
  90. package/src/bin-internal/exec.ts +28 -15
  91. package/src/bin-internal/fetch.ts +21 -10
  92. package/src/bin-internal/graph-backfill.ts +43 -0
  93. package/src/bin-internal/graph-query.ts +43 -0
  94. package/src/bin-internal/graph-record-script.ts +191 -0
  95. package/src/bin-internal/graph-record.ts +243 -0
  96. package/src/bin-internal/graph-snapshot.ts +23 -0
  97. package/src/bin-internal/heal-prepare.ts +230 -0
  98. package/src/bin-internal/index.ts +33 -11
  99. package/src/bin-internal/lint.ts +11 -4
  100. package/src/bin-internal/normalize.ts +23 -9
  101. package/src/bin-internal/post.ts +10 -4
  102. package/src/bin-internal/report.ts +3 -3
  103. package/src/bin-internal/status-cmd.ts +11 -3
  104. package/src/bin-internal/typecheck.ts +9 -3
  105. package/src/bin-internal/unlock.ts +12 -4
  106. package/src/bin-internal/validate-feature.ts +14 -5
  107. package/src/bin-internal/verify-prompts.ts +60 -0
  108. package/src/classifier/aggregate.ts +13 -6
  109. package/src/config/define.ts +3 -1
  110. package/src/config/load.ts +1 -1
  111. package/src/config/schema.ts +43 -37
  112. package/src/eval/paths.ts +32 -0
  113. package/src/eval/run-id.ts +30 -0
  114. package/src/eval/types.ts +101 -0
  115. package/src/graph/cost.ts +59 -0
  116. package/src/graph/index.ts +15 -0
  117. package/src/graph/paths.ts +27 -0
  118. package/src/graph/schema.ts +135 -0
  119. package/src/graph/store.ts +231 -0
  120. package/src/graph/types.ts +174 -0
  121. package/src/graph/ulid.ts +58 -0
  122. package/src/index.ts +11 -11
  123. package/src/jira/client.ts +4 -2
  124. package/src/jira/fields.ts +4 -2
  125. package/src/jira/mcp-backend.ts +1 -1
  126. package/src/jira/rest-backend.ts +18 -6
  127. package/src/jira/retry.ts +2 -2
  128. package/src/lock/file-lock.ts +2 -2
  129. package/src/logging/ndjson-logger.ts +2 -2
  130. package/src/reporter/jira-comment.ts +13 -7
  131. package/src/reporter/status-writer.ts +2 -2
package/dist/src/index.js CHANGED
@@ -1,120 +1,25 @@
1
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
- }
2
+ var __defProp = Object.defineProperty;
3
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
4
+ var __returnValue = (v) => v;
5
+ function __exportSetter(name, newValue) {
6
+ this[name] = __returnValue.bind(null, newValue);
7
+ }
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, {
11
+ get: all[name],
12
+ enumerable: true,
13
+ configurable: true,
14
+ set: __exportSetter.bind(all, name)
15
+ });
16
+ };
17
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
+ var __require = import.meta.require;
19
+
115
20
  // src/artifact/hash.ts
116
21
  import { createHash } from "crypto";
117
- import { existsSync as existsSync2, readFileSync } from "fs";
22
+ import { existsSync, readFileSync } from "fs";
118
23
  function hashString(s) {
119
24
  return `sha256:${createHash("sha256").update(s).digest("hex")}`;
120
25
  }
@@ -122,30 +27,30 @@ function hashFile(path) {
122
27
  return hashString(readFileSync(path, "utf8"));
123
28
  }
124
29
  function hashFileIfExists(path) {
125
- if (!existsSync2(path))
30
+ if (!existsSync(path))
126
31
  return null;
127
32
  return hashFile(path);
128
33
  }
129
34
  // src/artifact/meta.ts
130
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
35
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
131
36
  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()
37
+ import { z } from "zod";
38
+ var MetaJsonSchema = z.object({
39
+ ticket: z.string(),
40
+ adapter: z.string(),
41
+ xera_version: z.string(),
42
+ prompts_version: z.string(),
43
+ fetched_at: z.string().optional(),
44
+ story_hash: z.string().optional(),
45
+ feature_generated_at: z.string().optional(),
46
+ feature_generated_from_story_hash: z.string().optional(),
47
+ feature_hash: z.string().optional(),
48
+ script_generated_at: z.string().optional(),
49
+ script_generated_from_feature_hash: z.string().optional(),
50
+ script_warnings: z.array(z.string()).optional()
146
51
  });
147
52
  function readMeta(path) {
148
- if (!existsSync3(path))
53
+ if (!existsSync2(path))
149
54
  return null;
150
55
  return MetaJsonSchema.parse(JSON.parse(readFileSync2(path, "utf8")));
151
56
  }
@@ -162,36 +67,72 @@ function updateMeta(path, patch) {
162
67
  writeMeta(path, next);
163
68
  return next;
164
69
  }
70
+ // src/artifact/paths.ts
71
+ import { join } from "path";
72
+ var TICKET_RE = /^[A-Z][A-Z0-9_]*-\d+$|^SAMPLE-\d+$/;
73
+ function resolveArtifactPaths(repoRoot, ticket) {
74
+ if (!TICKET_RE.test(ticket)) {
75
+ throw new Error(`Invalid ticket key: "${ticket}" (expected e.g. JIRA-123 or SAMPLE-001)`);
76
+ }
77
+ const ticketDir = join(repoRoot, ".xera", ticket);
78
+ return {
79
+ ticketDir,
80
+ storyPath: join(ticketDir, "story.md"),
81
+ featurePath: join(ticketDir, "test.feature"),
82
+ specPath: join(ticketDir, "spec.ts"),
83
+ pageObjectsDir: join(ticketDir, "page-objects"),
84
+ runsDir: join(ticketDir, "runs"),
85
+ metaPath: join(ticketDir, "meta.json"),
86
+ statusPath: join(ticketDir, "status.json"),
87
+ logPath: join(ticketDir, "xera.log"),
88
+ lockPath: join(ticketDir, ".lock"),
89
+ authDir: join(repoRoot, ".xera", ".auth"),
90
+ runPath: (runId) => {
91
+ const runDir = join(ticketDir, "runs", runId);
92
+ return {
93
+ runDir,
94
+ reportJsonPath: join(runDir, "report.json"),
95
+ tracePath: join(runDir, "trace.zip"),
96
+ normalizedPath: join(runDir, "normalized.json"),
97
+ screenshotsDir: join(runDir, "screenshots"),
98
+ videoDir: join(runDir, "videos")
99
+ };
100
+ }
101
+ };
102
+ }
103
+ function generateRunId(now = new Date) {
104
+ return now.toISOString().replace(/[:.]/g, "-").replace("Z", "");
105
+ }
165
106
  // src/artifact/status.ts
166
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
107
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
167
108
  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(),
109
+ import { z as z2 } from "zod";
110
+ var ClassificationEnum = z2.enum(["PASS", "REAL_BUG", "SELECTOR_DRIFT", "FLAKY", "TEST_BUG"]);
111
+ var ResultEnum = z2.enum(["PASS", "FAIL"]);
112
+ var ConfidenceEnum = z2.enum(["low", "medium", "high"]);
113
+ var HistoryEntrySchema = z2.object({
114
+ ts: z2.string(),
174
115
  result: ResultEnum,
175
116
  class: ClassificationEnum
176
117
  });
177
- var StatusJsonSchema = z3.object({
178
- ticket: z3.string(),
179
- lastRun: z3.string(),
118
+ var StatusJsonSchema = z2.object({
119
+ ticket: z2.string(),
120
+ lastRun: z2.string(),
180
121
  result: ResultEnum,
181
122
  classification: ClassificationEnum,
182
123
  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()
124
+ scenarios: z2.object({
125
+ total: z2.number().int().nonnegative(),
126
+ passed: z2.number().int().nonnegative(),
127
+ failed: z2.number().int().nonnegative(),
128
+ skipped: z2.number().int().nonnegative()
188
129
  }),
189
- history: z3.array(HistoryEntrySchema).default([]),
190
- last_jira_comment_id: z3.string().optional()
130
+ history: z2.array(HistoryEntrySchema).default([]),
131
+ last_jira_comment_id: z2.string().optional()
191
132
  });
192
133
  var HISTORY_CAP = 20;
193
134
  function readStatus(path) {
194
- if (!existsSync4(path))
135
+ if (!existsSync3(path))
195
136
  return null;
196
137
  return StatusJsonSchema.parse(JSON.parse(readFileSync3(path, "utf8")));
197
138
  }
@@ -208,105 +149,219 @@ function appendHistory(path, entry) {
208
149
  writeStatus(path, s);
209
150
  return s;
210
151
  }
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
- `);
152
+ // src/auth/encrypt.ts
153
+ import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
154
+ var ALGO = "aes-256-gcm";
155
+ var KEY_LEN = 32;
156
+ var IV_LEN = 12;
157
+ var TAG_LEN = 16;
158
+ var VERSION = "v1";
159
+ function generateKey() {
160
+ return randomBytes(KEY_LEN).toString("hex");
161
+ }
162
+ function keyToBuf(key) {
163
+ const buf = Buffer.from(key, "hex");
164
+ if (buf.length !== KEY_LEN)
165
+ throw new Error(`Key must be ${KEY_LEN} bytes (got ${buf.length})`);
166
+ return buf;
167
+ }
168
+ function encrypt(plaintext, keyHex) {
169
+ const key = keyToBuf(keyHex);
170
+ const iv = randomBytes(IV_LEN);
171
+ const cipher = createCipheriv(ALGO, key, iv);
172
+ const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
173
+ const tag = cipher.getAuthTag();
174
+ return `${VERSION}:${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
175
+ }
176
+ function decrypt(ciphertext, keyHex) {
177
+ const [version, ivB64, tagB64, ctB64] = ciphertext.split(":");
178
+ if (version !== VERSION)
179
+ throw new Error(`Unsupported ciphertext version: ${version}`);
180
+ if (!ivB64 || !tagB64 || !ctB64)
181
+ throw new Error("Malformed ciphertext");
182
+ const key = keyToBuf(keyHex);
183
+ const iv = Buffer.from(ivB64, "base64");
184
+ const tag = Buffer.from(tagB64, "base64");
185
+ const ct = Buffer.from(ctB64, "base64");
186
+ if (tag.length !== TAG_LEN)
187
+ throw new Error("Bad auth tag length");
188
+ const decipher = createDecipheriv(ALGO, key, iv);
189
+ decipher.setAuthTag(tag);
190
+ return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8");
191
+ }
192
+ // src/auth/key.ts
193
+ var AUTH_KEY_ENV = "XERA_AUTH_KEY";
194
+ function resolveAuthKey() {
195
+ const key = process.env[AUTH_KEY_ENV];
196
+ if (!key) {
197
+ 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.`);
225
198
  }
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));
199
+ if (!/^[0-9a-f]{64}$/i.test(key)) {
200
+ throw new Error(`${AUTH_KEY_ENV} must be a 64-character hex string (32 bytes).`);
234
201
  }
202
+ return key;
235
203
  }
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" });
204
+ // src/auth/refresh.ts
205
+ var RE = /^(\d+)([hms])$/;
206
+ function parseDuration(d) {
207
+ const m = RE.exec(d);
208
+ if (!m)
209
+ throw new Error(`Bad duration "${d}" \u2014 expected e.g. "8h", "30m", "45s"`);
210
+ const n = Number(m[1]);
211
+ const unit = m[2];
212
+ if (unit === "h")
213
+ return n * 3600 * 1000;
214
+ if (unit === "m")
215
+ return n * 60 * 1000;
216
+ return n * 1000;
217
+ }
218
+ function needsRefresh(entry, policy, now = new Date) {
219
+ if (!entry)
252
220
  return true;
253
- } catch {
254
- return false;
255
- }
221
+ const ttlMs = parseDuration(policy.ttl);
222
+ const bufMs = parseDuration(policy.refreshBuffer);
223
+ const createdAt = new Date(entry.created_at).getTime();
224
+ if (now.getTime() - createdAt > ttlMs)
225
+ return true;
226
+ const expiresAt = new Date(entry.expires_at).getTime();
227
+ if (expiresAt - now.getTime() < bufMs)
228
+ return true;
229
+ return false;
256
230
  }
257
- function releaseLock(path) {
258
- if (existsSync6(path))
259
- unlinkSync(path);
231
+ // src/auth/state.ts
232
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
233
+ import { join as join2 } from "path";
234
+ import { z as z3 } from "zod";
235
+ var AuthStateEntrySchema = z3.object({
236
+ role: z3.string(),
237
+ strategy: z3.enum(["storageState", "apiToken"]),
238
+ created_at: z3.string(),
239
+ expires_at: z3.string(),
240
+ payload: z3.record(z3.string(), z3.unknown())
241
+ });
242
+ function pathFor(authDir, role) {
243
+ return join2(authDir, `${role}.json`);
260
244
  }
261
- function readLock(path) {
262
- if (!existsSync6(path))
245
+ function writeAuthState(authDir, entry) {
246
+ mkdirSync3(authDir, { recursive: true });
247
+ const ct = encrypt(JSON.stringify(entry), resolveAuthKey());
248
+ writeFileSync3(pathFor(authDir, entry.role), ct);
249
+ }
250
+ function readAuthState(authDir, role) {
251
+ const p = pathFor(authDir, role);
252
+ if (!existsSync4(p))
263
253
  return null;
264
- return JSON.parse(readFileSync5(path, "utf8"));
254
+ const txt = readFileSync4(p, "utf8");
255
+ const plain = decrypt(txt, resolveAuthKey());
256
+ return AuthStateEntrySchema.parse(JSON.parse(plain));
265
257
  }
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
- }
258
+ // src/config/define.ts
259
+ function defineConfig(config) {
260
+ return config;
279
261
  }
280
- function forceUnlock(path) {
281
- releaseLock(path);
262
+ // src/config/load.ts
263
+ import { existsSync as existsSync5 } from "fs";
264
+ import { join as join3 } from "path";
265
+ import { pathToFileURL } from "url";
266
+
267
+ // src/config/schema.ts
268
+ import { z as z4 } from "zod";
269
+ var AuthRoleSchema = z4.object({
270
+ envEmail: z4.string().min(1),
271
+ envPassword: z4.string().min(1)
272
+ });
273
+ var AuthSchema = z4.object({
274
+ strategy: z4.enum(["storageState", "apiToken", "none"]).default("none"),
275
+ ttl: z4.string().default("8h"),
276
+ refreshBuffer: z4.string().default("30m"),
277
+ setupScript: z4.string().optional(),
278
+ roles: z4.record(z4.string(), AuthRoleSchema).default({})
279
+ });
280
+ var WebSchema = z4.object({
281
+ baseUrl: z4.record(z4.string(), z4.string().url()).refine((m) => Object.keys(m).length > 0, {
282
+ message: "baseUrl must have at least one environment"
283
+ }),
284
+ defaultEnv: z4.string(),
285
+ auth: AuthSchema.prefault({}),
286
+ testData: z4.object({
287
+ users: z4.record(z4.string(), z4.object({ fromAuth: z4.string() })).default({})
288
+ }).prefault({})
289
+ }).refine((w) => w.baseUrl[w.defaultEnv] !== undefined, {
290
+ message: "defaultEnv must exist in baseUrl map",
291
+ path: ["defaultEnv"]
292
+ });
293
+ var JiraSchema = z4.object({
294
+ baseUrl: z4.string().url(),
295
+ projectKeys: z4.array(z4.string().min(1)).min(1),
296
+ fields: z4.object({
297
+ story: z4.string().min(1),
298
+ acceptanceCriteria: z4.string().optional(),
299
+ attachments: z4.string().default("attachment")
300
+ })
301
+ });
302
+ var AISchema = z4.object({
303
+ livePageSnapshot: z4.boolean().default(true),
304
+ confidenceThreshold: z4.enum(["low", "medium", "high"]).default("medium"),
305
+ maxRetries: z4.object({
306
+ typecheck: z4.number().int().min(0).max(5).default(2),
307
+ lint: z4.number().int().min(0).max(5).default(2),
308
+ validateFeature: z4.number().int().min(0).max(5).default(2)
309
+ }).prefault({})
310
+ }).prefault({});
311
+ var ReportingSchema = z4.object({
312
+ language: z4.enum(["en", "vi"]).default("en"),
313
+ postToJira: z4.boolean().default(true),
314
+ transition: z4.object({
315
+ onPass: z4.string().nullable().default(null),
316
+ onFail: z4.string().nullable().default(null)
317
+ }).prefault({}),
318
+ artifactLinks: z4.enum(["git", "local"]).default("git")
319
+ }).prefault({});
320
+ var XeraConfigSchema = z4.object({
321
+ jira: JiraSchema,
322
+ web: WebSchema,
323
+ ai: AISchema,
324
+ reporting: ReportingSchema,
325
+ adapters: z4.array(z4.string().min(1)).min(1).default(["web"])
326
+ });
327
+
328
+ // src/config/load.ts
329
+ async function loadConfig(cwd) {
330
+ const path = join3(cwd, "xera.config.ts");
331
+ if (!existsSync5(path)) {
332
+ throw new Error(`xera.config.ts not found in ${cwd}`);
333
+ }
334
+ const mod = await import(pathToFileURL(path).href);
335
+ const raw = mod.default ?? mod;
336
+ return XeraConfigSchema.parse(raw);
282
337
  }
283
338
  // src/jira/mcp-backend.ts
284
- import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5 } from "fs";
339
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
285
340
  import { tmpdir } from "os";
286
- import { join as join3 } from "path";
341
+ import { join as join4 } from "path";
287
342
  var MCP_ENV = "XERA_MCP_JIRA";
288
343
  async function createMcpBackend(_baseUrl) {
289
344
  if (process.env[MCP_ENV] !== "1")
290
345
  return null;
291
- const tmpDir = join3(tmpdir(), "xera-mcp");
292
- mkdirSync5(tmpDir, { recursive: true });
346
+ const tmpDir = join4(tmpdir(), "xera-mcp");
347
+ mkdirSync4(tmpDir, { recursive: true });
293
348
  return {
294
349
  backend: "mcp",
295
350
  async fetchTicket(key, _fields) {
296
- const cachePath = join3(tmpDir, `${key}.json`);
297
- if (!existsSync7(cachePath)) {
351
+ const cachePath = join4(tmpDir, `${key}.json`);
352
+ if (!existsSync6(cachePath)) {
298
353
  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
354
  }
300
- const parsed = JSON.parse(readFileSync6(cachePath, "utf8"));
355
+ const parsed = JSON.parse(readFileSync5(cachePath, "utf8"));
301
356
  return parsed;
302
357
  },
303
358
  async postComment(key, body) {
304
- const outPath = join3(tmpDir, `${key}.comment.json`);
359
+ const outPath = join4(tmpDir, `${key}.comment.json`);
305
360
  writeFileSync4(outPath, JSON.stringify({ key, body }));
306
361
  return { id: "mcp-pending" };
307
362
  },
308
363
  async transitionStatus(key, statusName) {
309
- const outPath = join3(tmpDir, `${key}.transition.json`);
364
+ const outPath = join4(tmpDir, `${key}.transition.json`);
310
365
  writeFileSync4(outPath, JSON.stringify({ key, statusName }));
311
366
  },
312
367
  async listFields(_sampleKey) {
@@ -344,7 +399,10 @@ function createRestBackend(baseUrl, creds) {
344
399
  const r = await req(`/rest/api/3/issue/${encodeURIComponent(key)}?fields=${want.join(",")}`);
345
400
  const json = await r.json();
346
401
  const f = json.fields;
347
- const attachments = Array.isArray(f.attachment) ? f.attachment.map((a) => ({ filename: a.filename, url: a.content })) : [];
402
+ const attachments = Array.isArray(f.attachment) ? f.attachment.map((a) => ({
403
+ filename: a.filename,
404
+ url: a.content
405
+ })) : [];
348
406
  const ticket = {
349
407
  key: json.key,
350
408
  summary: String(f.summary ?? ""),
@@ -361,7 +419,11 @@ function createRestBackend(baseUrl, creds) {
361
419
  const r = await req(`/rest/api/3/issue/${encodeURIComponent(key)}/comment`, {
362
420
  method: "POST",
363
421
  body: JSON.stringify({
364
- body: { type: "doc", version: 1, content: [{ type: "paragraph", content: [{ type: "text", text: body }] }] }
422
+ body: {
423
+ type: "doc",
424
+ version: 1,
425
+ content: [{ type: "paragraph", content: [{ type: "text", text: body }] }]
426
+ }
365
427
  })
366
428
  });
367
429
  const json = await r.json();
@@ -431,117 +493,83 @@ async function withRetry(fn, opts) {
431
493
  attempt++;
432
494
  if (attempt >= opts.maxAttempts)
433
495
  break;
434
- const delay = opts.baseMs * Math.pow(opts.factor, attempt - 1);
496
+ const delay = opts.baseMs * opts.factor ** (attempt - 1);
435
497
  await new Promise((r) => setTimeout(r, delay));
436
498
  }
437
499
  }
438
500
  throw lastErr;
439
501
  }
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).`);
502
+ // src/lock/file-lock.ts
503
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
504
+ import { hostname } from "os";
505
+ import { dirname as dirname3 } from "path";
506
+ function acquireLock(path, runId) {
507
+ if (existsSync7(path))
508
+ return false;
509
+ mkdirSync5(dirname3(path), { recursive: true });
510
+ const data = {
511
+ pid: process.pid,
512
+ hostname: hostname(),
513
+ started_at: new Date().toISOString(),
514
+ run_id: runId
515
+ };
516
+ try {
517
+ writeFileSync5(path, JSON.stringify(data), { flag: "wx" });
518
+ return true;
519
+ } catch {
520
+ return false;
489
521
  }
490
- return key;
491
522
  }
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);
523
+ function releaseLock(path) {
524
+ if (existsSync7(path))
525
+ unlinkSync(path);
510
526
  }
511
- function readAuthState(authDir, role) {
512
- const p = pathFor(authDir, role);
513
- if (!existsSync8(p))
527
+ function readLock(path) {
528
+ if (!existsSync7(path))
514
529
  return null;
515
- const txt = readFileSync7(p, "utf8");
516
- const plain = decrypt(txt, resolveAuthKey());
517
- return AuthStateEntrySchema.parse(JSON.parse(plain));
530
+ return JSON.parse(readFileSync6(path, "utf8"));
518
531
  }
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)
532
+ function isLockStale(path) {
533
+ const lock = readLock(path);
534
+ if (!lock)
540
535
  return true;
541
- const expiresAt = new Date(entry.expires_at).getTime();
542
- if (expiresAt - now.getTime() < bufMs)
536
+ if (lock.hostname !== hostname()) {
537
+ return false;
538
+ }
539
+ try {
540
+ process.kill(lock.pid, 0);
541
+ return false;
542
+ } catch {
543
543
  return true;
544
- return false;
544
+ }
545
+ }
546
+ function forceUnlock(path) {
547
+ releaseLock(path);
548
+ }
549
+ // src/logging/ndjson-logger.ts
550
+ import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync7 } from "fs";
551
+ import { dirname as dirname4 } from "path";
552
+
553
+ class NdjsonLogger {
554
+ path;
555
+ constructor(path) {
556
+ this.path = path;
557
+ mkdirSync6(dirname4(path), { recursive: true });
558
+ }
559
+ log(payload) {
560
+ const entry = { ts: new Date().toISOString(), ...payload };
561
+ appendFileSync(this.path, `${JSON.stringify(entry)}
562
+ `);
563
+ }
564
+ static readAll(path) {
565
+ if (!existsSync8(path))
566
+ return [];
567
+ const txt = readFileSync7(path, "utf8").trim();
568
+ if (!txt)
569
+ return [];
570
+ return txt.split(`
571
+ `).map((line) => JSON.parse(line));
572
+ }
545
573
  }
546
574
 
547
575
  // src/index.ts