@xera-ai/core 0.3.0 → 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.
- package/bin/internal.ts +1 -0
- package/dist/adapter/types.d.ts.map +1 -0
- package/dist/artifact/hash.d.ts.map +1 -0
- package/dist/artifact/meta.d.ts +20 -0
- package/dist/artifact/meta.d.ts.map +1 -0
- package/dist/artifact/paths.d.ts.map +1 -0
- package/dist/artifact/status.d.ts +71 -0
- package/dist/artifact/status.d.ts.map +1 -0
- package/dist/auth/encrypt.d.ts.map +1 -0
- package/dist/auth/key.d.ts.map +1 -0
- package/dist/auth/refresh.d.ts.map +1 -0
- package/dist/{core/src/auth → auth}/state.d.ts +5 -14
- package/dist/auth/state.d.ts.map +1 -0
- package/dist/bin/internal.js +8351 -369
- package/dist/bin-internal/doctor.d.ts.map +1 -0
- package/dist/bin-internal/eval-deterministic.d.ts.map +1 -0
- package/dist/bin-internal/eval-prepare.d.ts.map +1 -0
- package/dist/bin-internal/eval-report.d.ts.map +1 -0
- package/dist/bin-internal/exec.d.ts.map +1 -0
- package/dist/bin-internal/fetch.d.ts.map +1 -0
- package/dist/bin-internal/graph-backfill.d.ts +2 -0
- package/dist/bin-internal/graph-backfill.d.ts.map +1 -0
- package/dist/bin-internal/graph-query.d.ts +2 -0
- package/dist/bin-internal/graph-query.d.ts.map +1 -0
- package/dist/bin-internal/graph-record-script.d.ts +2 -0
- package/dist/bin-internal/graph-record-script.d.ts.map +1 -0
- package/dist/bin-internal/graph-record.d.ts +3 -0
- package/dist/bin-internal/graph-record.d.ts.map +1 -0
- package/dist/bin-internal/graph-snapshot.d.ts +2 -0
- package/dist/bin-internal/graph-snapshot.d.ts.map +1 -0
- package/dist/bin-internal/heal-prepare.d.ts.map +1 -0
- package/dist/bin-internal/index.d.ts.map +1 -0
- package/dist/bin-internal/lint.d.ts.map +1 -0
- package/dist/bin-internal/normalize.d.ts.map +1 -0
- package/dist/bin-internal/post.d.ts.map +1 -0
- package/dist/bin-internal/promote.d.ts.map +1 -0
- package/dist/bin-internal/report.d.ts.map +1 -0
- package/dist/bin-internal/status-cmd.d.ts.map +1 -0
- package/dist/bin-internal/typecheck.d.ts.map +1 -0
- package/dist/bin-internal/unlock.d.ts.map +1 -0
- package/dist/bin-internal/validate-feature.d.ts.map +1 -0
- package/dist/bin-internal/verify-prompts.d.ts.map +1 -0
- package/dist/classifier/aggregate.d.ts.map +1 -0
- package/dist/classifier/history.d.ts.map +1 -0
- package/dist/classifier/types.d.ts.map +1 -0
- package/dist/config/define.d.ts.map +1 -0
- package/dist/config/load.d.ts.map +1 -0
- package/dist/config/schema.d.ts +66 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/eval/paths.d.ts.map +1 -0
- package/dist/eval/run-id.d.ts.map +1 -0
- package/dist/eval/types.d.ts +203 -0
- package/dist/eval/types.d.ts.map +1 -0
- package/dist/graph/cost.d.ts +21 -0
- package/dist/graph/cost.d.ts.map +1 -0
- package/dist/graph/index.d.ts +8 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/paths.d.ts +10 -0
- package/dist/graph/paths.d.ts.map +1 -0
- package/dist/graph/schema.d.ts +177 -0
- package/dist/graph/schema.d.ts.map +1 -0
- package/dist/graph/store.d.ts +14 -0
- package/dist/graph/store.d.ts.map +1 -0
- package/dist/graph/types.d.ts +151 -0
- package/dist/graph/types.d.ts.map +1 -0
- package/dist/graph/ulid.d.ts +2 -0
- package/dist/graph/ulid.d.ts.map +1 -0
- package/dist/{core/src/index.d.ts → index.d.ts} +11 -11
- package/dist/index.d.ts.map +1 -0
- package/dist/jira/client.d.ts.map +1 -0
- package/dist/jira/fields.d.ts.map +1 -0
- package/dist/jira/mcp-backend.d.ts.map +1 -0
- package/dist/jira/rest-backend.d.ts.map +1 -0
- package/dist/jira/retry.d.ts.map +1 -0
- package/dist/jira/types.d.ts.map +1 -0
- package/dist/lock/file-lock.d.ts.map +1 -0
- package/dist/logging/ndjson-logger.d.ts.map +1 -0
- package/dist/reporter/jira-comment.d.ts.map +1 -0
- package/dist/reporter/status-writer.d.ts.map +1 -0
- package/dist/src/index.js +339 -318
- package/package.json +19 -14
- package/src/auth/refresh.ts +1 -0
- package/src/bin-internal/doctor.ts +37 -1
- package/src/bin-internal/eval-prepare.ts +1 -1
- package/src/bin-internal/graph-backfill.ts +43 -0
- package/src/bin-internal/graph-query.ts +43 -0
- package/src/bin-internal/graph-record-script.ts +191 -0
- package/src/bin-internal/graph-record.ts +243 -0
- package/src/bin-internal/graph-snapshot.ts +23 -0
- package/src/bin-internal/heal-prepare.ts +1 -1
- package/src/bin-internal/index.ts +8 -0
- package/src/bin-internal/verify-prompts.ts +1 -0
- package/src/config/schema.ts +6 -6
- package/src/graph/cost.ts +59 -0
- package/src/graph/index.ts +15 -0
- package/src/graph/paths.ts +27 -0
- package/src/graph/schema.ts +135 -0
- package/src/graph/store.ts +231 -0
- package/src/graph/types.ts +174 -0
- package/src/graph/ulid.ts +58 -0
- package/src/index.ts +11 -11
- package/src/jira/rest-backend.ts +1 -1
- package/src/reporter/status-writer.ts +1 -1
- package/dist/core/src/adapter/types.d.ts.map +0 -1
- package/dist/core/src/artifact/hash.d.ts.map +0 -1
- package/dist/core/src/artifact/meta.d.ts +0 -46
- package/dist/core/src/artifact/meta.d.ts.map +0 -1
- package/dist/core/src/artifact/paths.d.ts.map +0 -1
- package/dist/core/src/artifact/status.d.ts +0 -96
- package/dist/core/src/artifact/status.d.ts.map +0 -1
- package/dist/core/src/auth/encrypt.d.ts.map +0 -1
- package/dist/core/src/auth/key.d.ts.map +0 -1
- package/dist/core/src/auth/refresh.d.ts.map +0 -1
- package/dist/core/src/auth/state.d.ts.map +0 -1
- package/dist/core/src/bin-internal/doctor.d.ts.map +0 -1
- package/dist/core/src/bin-internal/eval-deterministic.d.ts.map +0 -1
- package/dist/core/src/bin-internal/eval-prepare.d.ts.map +0 -1
- package/dist/core/src/bin-internal/eval-report.d.ts.map +0 -1
- package/dist/core/src/bin-internal/exec.d.ts.map +0 -1
- package/dist/core/src/bin-internal/fetch.d.ts.map +0 -1
- package/dist/core/src/bin-internal/heal-prepare.d.ts.map +0 -1
- package/dist/core/src/bin-internal/index.d.ts.map +0 -1
- package/dist/core/src/bin-internal/lint.d.ts.map +0 -1
- package/dist/core/src/bin-internal/normalize.d.ts.map +0 -1
- package/dist/core/src/bin-internal/post.d.ts.map +0 -1
- package/dist/core/src/bin-internal/promote.d.ts.map +0 -1
- package/dist/core/src/bin-internal/report.d.ts.map +0 -1
- package/dist/core/src/bin-internal/status-cmd.d.ts.map +0 -1
- package/dist/core/src/bin-internal/typecheck.d.ts.map +0 -1
- package/dist/core/src/bin-internal/unlock.d.ts.map +0 -1
- package/dist/core/src/bin-internal/validate-feature.d.ts.map +0 -1
- package/dist/core/src/bin-internal/verify-prompts.d.ts.map +0 -1
- package/dist/core/src/classifier/aggregate.d.ts.map +0 -1
- package/dist/core/src/classifier/history.d.ts.map +0 -1
- package/dist/core/src/classifier/types.d.ts.map +0 -1
- package/dist/core/src/config/define.d.ts.map +0 -1
- package/dist/core/src/config/load.d.ts.map +0 -1
- package/dist/core/src/config/schema.d.ts +0 -326
- package/dist/core/src/config/schema.d.ts.map +0 -1
- package/dist/core/src/eval/paths.d.ts.map +0 -1
- package/dist/core/src/eval/run-id.d.ts.map +0 -1
- package/dist/core/src/eval/types.d.ts +0 -551
- package/dist/core/src/eval/types.d.ts.map +0 -1
- package/dist/core/src/index.d.ts.map +0 -1
- package/dist/core/src/jira/client.d.ts.map +0 -1
- package/dist/core/src/jira/fields.d.ts.map +0 -1
- package/dist/core/src/jira/mcp-backend.d.ts.map +0 -1
- package/dist/core/src/jira/rest-backend.d.ts.map +0 -1
- package/dist/core/src/jira/retry.d.ts.map +0 -1
- package/dist/core/src/jira/types.d.ts.map +0 -1
- package/dist/core/src/lock/file-lock.d.ts.map +0 -1
- package/dist/core/src/logging/ndjson-logger.d.ts.map +0 -1
- package/dist/core/src/reporter/jira-comment.d.ts.map +0 -1
- package/dist/core/src/reporter/status-writer.d.ts.map +0 -1
- package/dist/web/src/adapter.d.ts +0 -3
- package/dist/web/src/adapter.d.ts.map +0 -1
- package/dist/web/src/auth-setup/define.d.ts +0 -16
- package/dist/web/src/auth-setup/define.d.ts.map +0 -1
- package/dist/web/src/auth-setup/playwright-state.d.ts +0 -2
- package/dist/web/src/auth-setup/playwright-state.d.ts.map +0 -1
- package/dist/web/src/auth-setup/runner.d.ts +0 -12
- package/dist/web/src/auth-setup/runner.d.ts.map +0 -1
- package/dist/web/src/executor/index.d.ts +0 -18
- package/dist/web/src/executor/index.d.ts.map +0 -1
- package/dist/web/src/executor/playwright-args.d.ts +0 -7
- package/dist/web/src/executor/playwright-args.d.ts.map +0 -1
- package/dist/web/src/generator/gherkin-validate.d.ts +0 -9
- package/dist/web/src/generator/gherkin-validate.d.ts.map +0 -1
- package/dist/web/src/generator/lint.d.ts +0 -9
- package/dist/web/src/generator/lint.d.ts.map +0 -1
- package/dist/web/src/generator/pom-scan.d.ts +0 -6
- package/dist/web/src/generator/pom-scan.d.ts.map +0 -1
- package/dist/web/src/generator/promote.d.ts +0 -7
- package/dist/web/src/generator/promote.d.ts.map +0 -1
- package/dist/web/src/generator/selector-rules.d.ts +0 -10
- package/dist/web/src/generator/selector-rules.d.ts.map +0 -1
- package/dist/web/src/generator/typecheck.d.ts +0 -11
- package/dist/web/src/generator/typecheck.d.ts.map +0 -1
- package/dist/web/src/index.d.ts +0 -18
- package/dist/web/src/index.d.ts.map +0 -1
- package/dist/web/src/trace-normalizer/normalize.d.ts +0 -7
- package/dist/web/src/trace-normalizer/normalize.d.ts.map +0 -1
- package/dist/web/src/trace-normalizer/parse.d.ts +0 -37
- package/dist/web/src/trace-normalizer/parse.d.ts.map +0 -1
- package/dist/web/src/trace-normalizer/scrub-rules.d.ts +0 -12
- package/dist/web/src/trace-normalizer/scrub-rules.d.ts.map +0 -1
- package/dist/web/src/trace-normalizer/scrub.d.ts +0 -29
- package/dist/web/src/trace-normalizer/scrub.d.ts.map +0 -1
- package/dist/web/src/trace-normalizer/unzip.d.ts +0 -6
- package/dist/web/src/trace-normalizer/unzip.d.ts.map +0 -1
- /package/dist/{core/src/adapter → adapter}/types.d.ts +0 -0
- /package/dist/{core/src/artifact → artifact}/hash.d.ts +0 -0
- /package/dist/{core/src/artifact → artifact}/paths.d.ts +0 -0
- /package/dist/{core/src/auth → auth}/encrypt.d.ts +0 -0
- /package/dist/{core/src/auth → auth}/key.d.ts +0 -0
- /package/dist/{core/src/auth → auth}/refresh.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/doctor.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/eval-deterministic.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/eval-prepare.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/eval-report.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/exec.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/fetch.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/heal-prepare.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/index.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/lint.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/normalize.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/post.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/promote.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/report.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/status-cmd.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/typecheck.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/unlock.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/validate-feature.d.ts +0 -0
- /package/dist/{core/src/bin-internal → bin-internal}/verify-prompts.d.ts +0 -0
- /package/dist/{core/src/classifier → classifier}/aggregate.d.ts +0 -0
- /package/dist/{core/src/classifier → classifier}/history.d.ts +0 -0
- /package/dist/{core/src/classifier → classifier}/types.d.ts +0 -0
- /package/dist/{core/src/config → config}/define.d.ts +0 -0
- /package/dist/{core/src/config → config}/load.d.ts +0 -0
- /package/dist/{core/src/eval → eval}/paths.d.ts +0 -0
- /package/dist/{core/src/eval → eval}/run-id.d.ts +0 -0
- /package/dist/{core/src/jira → jira}/client.d.ts +0 -0
- /package/dist/{core/src/jira → jira}/fields.d.ts +0 -0
- /package/dist/{core/src/jira → jira}/mcp-backend.d.ts +0 -0
- /package/dist/{core/src/jira → jira}/rest-backend.d.ts +0 -0
- /package/dist/{core/src/jira → jira}/retry.d.ts +0 -0
- /package/dist/{core/src/jira → jira}/types.d.ts +0 -0
- /package/dist/{core/src/lock → lock}/file-lock.d.ts +0 -0
- /package/dist/{core/src/logging → logging}/ndjson-logger.d.ts +0 -0
- /package/dist/{core/src/reporter → reporter}/jira-comment.d.ts +0 -0
- /package/dist/{core/src/reporter → reporter}/status-writer.d.ts +0 -0
package/dist/src/index.js
CHANGED
|
@@ -1,120 +1,25 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
var
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
var
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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 (!
|
|
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
|
|
35
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
131
36
|
import { dirname } from "path";
|
|
132
|
-
import { z
|
|
133
|
-
var MetaJsonSchema =
|
|
134
|
-
ticket:
|
|
135
|
-
adapter:
|
|
136
|
-
xera_version:
|
|
137
|
-
prompts_version:
|
|
138
|
-
fetched_at:
|
|
139
|
-
story_hash:
|
|
140
|
-
feature_generated_at:
|
|
141
|
-
feature_generated_from_story_hash:
|
|
142
|
-
feature_hash:
|
|
143
|
-
script_generated_at:
|
|
144
|
-
script_generated_from_feature_hash:
|
|
145
|
-
script_warnings:
|
|
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 (!
|
|
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
|
|
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
|
|
169
|
-
var ClassificationEnum =
|
|
170
|
-
var ResultEnum =
|
|
171
|
-
var ConfidenceEnum =
|
|
172
|
-
var HistoryEntrySchema =
|
|
173
|
-
ts:
|
|
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 =
|
|
178
|
-
ticket:
|
|
179
|
-
lastRun:
|
|
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:
|
|
184
|
-
total:
|
|
185
|
-
passed:
|
|
186
|
-
failed:
|
|
187
|
-
skipped:
|
|
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:
|
|
190
|
-
last_jira_comment_id:
|
|
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 (!
|
|
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/
|
|
212
|
-
import {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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/
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
|
262
|
-
|
|
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
|
-
|
|
254
|
+
const txt = readFileSync4(p, "utf8");
|
|
255
|
+
const plain = decrypt(txt, resolveAuthKey());
|
|
256
|
+
return AuthStateEntrySchema.parse(JSON.parse(plain));
|
|
265
257
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
281
|
-
|
|
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
|
|
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
|
|
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 =
|
|
292
|
-
|
|
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 =
|
|
297
|
-
if (!
|
|
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(
|
|
355
|
+
const parsed = JSON.parse(readFileSync5(cachePath, "utf8"));
|
|
301
356
|
return parsed;
|
|
302
357
|
},
|
|
303
358
|
async postComment(key, body) {
|
|
304
|
-
const outPath =
|
|
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 =
|
|
364
|
+
const outPath = join4(tmpDir, `${key}.transition.json`);
|
|
310
365
|
writeFileSync4(outPath, JSON.stringify({ key, statusName }));
|
|
311
366
|
},
|
|
312
367
|
async listFields(_sampleKey) {
|
|
@@ -444,111 +499,77 @@ async function withRetry(fn, opts) {
|
|
|
444
499
|
}
|
|
445
500
|
throw lastErr;
|
|
446
501
|
}
|
|
447
|
-
// src/
|
|
448
|
-
import {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
const cipher = createCipheriv(ALGO, key, iv);
|
|
467
|
-
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
468
|
-
const tag = cipher.getAuthTag();
|
|
469
|
-
return `${VERSION}:${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
|
|
470
|
-
}
|
|
471
|
-
function decrypt(ciphertext, keyHex) {
|
|
472
|
-
const [version, ivB64, tagB64, ctB64] = ciphertext.split(":");
|
|
473
|
-
if (version !== VERSION)
|
|
474
|
-
throw new Error(`Unsupported ciphertext version: ${version}`);
|
|
475
|
-
if (!ivB64 || !tagB64 || !ctB64)
|
|
476
|
-
throw new Error("Malformed ciphertext");
|
|
477
|
-
const key = keyToBuf(keyHex);
|
|
478
|
-
const iv = Buffer.from(ivB64, "base64");
|
|
479
|
-
const tag = Buffer.from(tagB64, "base64");
|
|
480
|
-
const ct = Buffer.from(ctB64, "base64");
|
|
481
|
-
if (tag.length !== TAG_LEN)
|
|
482
|
-
throw new Error("Bad auth tag length");
|
|
483
|
-
const decipher = createDecipheriv(ALGO, key, iv);
|
|
484
|
-
decipher.setAuthTag(tag);
|
|
485
|
-
return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8");
|
|
486
|
-
}
|
|
487
|
-
// src/auth/key.ts
|
|
488
|
-
var AUTH_KEY_ENV = "XERA_AUTH_KEY";
|
|
489
|
-
function resolveAuthKey() {
|
|
490
|
-
const key = process.env[AUTH_KEY_ENV];
|
|
491
|
-
if (!key) {
|
|
492
|
-
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.`);
|
|
493
|
-
}
|
|
494
|
-
if (!/^[0-9a-f]{64}$/i.test(key)) {
|
|
495
|
-
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;
|
|
496
521
|
}
|
|
497
|
-
return key;
|
|
498
522
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
import { z as z4 } from "zod";
|
|
503
|
-
var AuthStateEntrySchema = z4.object({
|
|
504
|
-
role: z4.string(),
|
|
505
|
-
strategy: z4.enum(["storageState", "apiToken"]),
|
|
506
|
-
created_at: z4.string(),
|
|
507
|
-
expires_at: z4.string(),
|
|
508
|
-
payload: z4.record(z4.string(), z4.unknown())
|
|
509
|
-
});
|
|
510
|
-
function pathFor(authDir, role) {
|
|
511
|
-
return join4(authDir, `${role}.json`);
|
|
512
|
-
}
|
|
513
|
-
function writeAuthState(authDir, entry) {
|
|
514
|
-
mkdirSync6(authDir, { recursive: true });
|
|
515
|
-
const ct = encrypt(JSON.stringify(entry), resolveAuthKey());
|
|
516
|
-
writeFileSync5(pathFor(authDir, entry.role), ct);
|
|
523
|
+
function releaseLock(path) {
|
|
524
|
+
if (existsSync7(path))
|
|
525
|
+
unlinkSync(path);
|
|
517
526
|
}
|
|
518
|
-
function
|
|
519
|
-
|
|
520
|
-
if (!existsSync8(p))
|
|
527
|
+
function readLock(path) {
|
|
528
|
+
if (!existsSync7(path))
|
|
521
529
|
return null;
|
|
522
|
-
|
|
523
|
-
const plain = decrypt(txt, resolveAuthKey());
|
|
524
|
-
return AuthStateEntrySchema.parse(JSON.parse(plain));
|
|
525
|
-
}
|
|
526
|
-
// src/auth/refresh.ts
|
|
527
|
-
var RE = /^(\d+)([hms])$/;
|
|
528
|
-
function parseDuration(d) {
|
|
529
|
-
const m = RE.exec(d);
|
|
530
|
-
if (!m)
|
|
531
|
-
throw new Error(`Bad duration "${d}" \u2014 expected e.g. "8h", "30m", "45s"`);
|
|
532
|
-
const n = Number(m[1]);
|
|
533
|
-
const unit = m[2];
|
|
534
|
-
if (unit === "h")
|
|
535
|
-
return n * 3600 * 1000;
|
|
536
|
-
if (unit === "m")
|
|
537
|
-
return n * 60 * 1000;
|
|
538
|
-
return n * 1000;
|
|
530
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
539
531
|
}
|
|
540
|
-
function
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const ttlMs = parseDuration(policy.ttl);
|
|
544
|
-
const bufMs = parseDuration(policy.refreshBuffer);
|
|
545
|
-
const createdAt = new Date(entry.created_at).getTime();
|
|
546
|
-
if (now.getTime() - createdAt > ttlMs)
|
|
532
|
+
function isLockStale(path) {
|
|
533
|
+
const lock = readLock(path);
|
|
534
|
+
if (!lock)
|
|
547
535
|
return true;
|
|
548
|
-
|
|
549
|
-
|
|
536
|
+
if (lock.hostname !== hostname()) {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
try {
|
|
540
|
+
process.kill(lock.pid, 0);
|
|
541
|
+
return false;
|
|
542
|
+
} catch {
|
|
550
543
|
return true;
|
|
551
|
-
|
|
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
|
+
}
|
|
552
573
|
}
|
|
553
574
|
|
|
554
575
|
// src/index.ts
|