@xera-ai/core 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +75 -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 +8607 -373
- 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-enrich.d.ts +2 -0
- package/dist/bin-internal/graph-enrich.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/classify.d.ts +42 -0
- package/dist/graph/classify.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/enrich.d.ts +10 -0
- package/dist/graph/enrich.d.ts.map +1 -0
- package/dist/graph/index.d.ts +13 -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 +180 -0
- package/dist/graph/schema.d.ts.map +1 -0
- package/dist/graph/similarity.d.ts +3 -0
- package/dist/graph/similarity.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 +346 -318
- package/package.json +19 -14
- package/src/artifact/status.ts +8 -1
- 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-enrich.ts +28 -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 +287 -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 +10 -0
- package/src/bin-internal/report.ts +63 -5
- package/src/bin-internal/verify-prompts.ts +3 -0
- package/src/classifier/aggregate.ts +1 -0
- package/src/config/schema.ts +6 -6
- package/src/graph/classify.ts +126 -0
- package/src/graph/cost.ts +59 -0
- package/src/graph/enrich.ts +103 -0
- package/src/graph/index.ts +30 -0
- package/src/graph/paths.ts +27 -0
- package/src/graph/schema.ts +142 -0
- package/src/graph/similarity.ts +43 -0
- package/src/graph/store.ts +231 -0
- package/src/graph/types.ts +179 -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,79 @@ 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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
109
|
+
import { z as z2 } from "zod";
|
|
110
|
+
var ClassificationEnum = z2.enum([
|
|
111
|
+
"PASS",
|
|
112
|
+
"REAL_BUG",
|
|
113
|
+
"SELECTOR_DRIFT",
|
|
114
|
+
"FLAKY",
|
|
115
|
+
"TEST_BUG",
|
|
116
|
+
"TEST_OUTDATED"
|
|
117
|
+
]);
|
|
118
|
+
var ResultEnum = z2.enum(["PASS", "FAIL"]);
|
|
119
|
+
var ConfidenceEnum = z2.enum(["low", "medium", "high"]);
|
|
120
|
+
var HistoryEntrySchema = z2.object({
|
|
121
|
+
ts: z2.string(),
|
|
174
122
|
result: ResultEnum,
|
|
175
123
|
class: ClassificationEnum
|
|
176
124
|
});
|
|
177
|
-
var StatusJsonSchema =
|
|
178
|
-
ticket:
|
|
179
|
-
lastRun:
|
|
125
|
+
var StatusJsonSchema = z2.object({
|
|
126
|
+
ticket: z2.string(),
|
|
127
|
+
lastRun: z2.string(),
|
|
180
128
|
result: ResultEnum,
|
|
181
129
|
classification: ClassificationEnum,
|
|
182
130
|
confidence: ConfidenceEnum,
|
|
183
|
-
scenarios:
|
|
184
|
-
total:
|
|
185
|
-
passed:
|
|
186
|
-
failed:
|
|
187
|
-
skipped:
|
|
131
|
+
scenarios: z2.object({
|
|
132
|
+
total: z2.number().int().nonnegative(),
|
|
133
|
+
passed: z2.number().int().nonnegative(),
|
|
134
|
+
failed: z2.number().int().nonnegative(),
|
|
135
|
+
skipped: z2.number().int().nonnegative()
|
|
188
136
|
}),
|
|
189
|
-
history:
|
|
190
|
-
last_jira_comment_id:
|
|
137
|
+
history: z2.array(HistoryEntrySchema).default([]),
|
|
138
|
+
last_jira_comment_id: z2.string().optional()
|
|
191
139
|
});
|
|
192
140
|
var HISTORY_CAP = 20;
|
|
193
141
|
function readStatus(path) {
|
|
194
|
-
if (!
|
|
142
|
+
if (!existsSync3(path))
|
|
195
143
|
return null;
|
|
196
144
|
return StatusJsonSchema.parse(JSON.parse(readFileSync3(path, "utf8")));
|
|
197
145
|
}
|
|
@@ -208,105 +156,219 @@ function appendHistory(path, entry) {
|
|
|
208
156
|
writeStatus(path, s);
|
|
209
157
|
return s;
|
|
210
158
|
}
|
|
211
|
-
// src/
|
|
212
|
-
import {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
`);
|
|
159
|
+
// src/auth/encrypt.ts
|
|
160
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
161
|
+
var ALGO = "aes-256-gcm";
|
|
162
|
+
var KEY_LEN = 32;
|
|
163
|
+
var IV_LEN = 12;
|
|
164
|
+
var TAG_LEN = 16;
|
|
165
|
+
var VERSION = "v1";
|
|
166
|
+
function generateKey() {
|
|
167
|
+
return randomBytes(KEY_LEN).toString("hex");
|
|
168
|
+
}
|
|
169
|
+
function keyToBuf(key) {
|
|
170
|
+
const buf = Buffer.from(key, "hex");
|
|
171
|
+
if (buf.length !== KEY_LEN)
|
|
172
|
+
throw new Error(`Key must be ${KEY_LEN} bytes (got ${buf.length})`);
|
|
173
|
+
return buf;
|
|
174
|
+
}
|
|
175
|
+
function encrypt(plaintext, keyHex) {
|
|
176
|
+
const key = keyToBuf(keyHex);
|
|
177
|
+
const iv = randomBytes(IV_LEN);
|
|
178
|
+
const cipher = createCipheriv(ALGO, key, iv);
|
|
179
|
+
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
180
|
+
const tag = cipher.getAuthTag();
|
|
181
|
+
return `${VERSION}:${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
|
|
182
|
+
}
|
|
183
|
+
function decrypt(ciphertext, keyHex) {
|
|
184
|
+
const [version, ivB64, tagB64, ctB64] = ciphertext.split(":");
|
|
185
|
+
if (version !== VERSION)
|
|
186
|
+
throw new Error(`Unsupported ciphertext version: ${version}`);
|
|
187
|
+
if (!ivB64 || !tagB64 || !ctB64)
|
|
188
|
+
throw new Error("Malformed ciphertext");
|
|
189
|
+
const key = keyToBuf(keyHex);
|
|
190
|
+
const iv = Buffer.from(ivB64, "base64");
|
|
191
|
+
const tag = Buffer.from(tagB64, "base64");
|
|
192
|
+
const ct = Buffer.from(ctB64, "base64");
|
|
193
|
+
if (tag.length !== TAG_LEN)
|
|
194
|
+
throw new Error("Bad auth tag length");
|
|
195
|
+
const decipher = createDecipheriv(ALGO, key, iv);
|
|
196
|
+
decipher.setAuthTag(tag);
|
|
197
|
+
return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8");
|
|
198
|
+
}
|
|
199
|
+
// src/auth/key.ts
|
|
200
|
+
var AUTH_KEY_ENV = "XERA_AUTH_KEY";
|
|
201
|
+
function resolveAuthKey() {
|
|
202
|
+
const key = process.env[AUTH_KEY_ENV];
|
|
203
|
+
if (!key) {
|
|
204
|
+
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
205
|
}
|
|
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));
|
|
206
|
+
if (!/^[0-9a-f]{64}$/i.test(key)) {
|
|
207
|
+
throw new Error(`${AUTH_KEY_ENV} must be a 64-character hex string (32 bytes).`);
|
|
234
208
|
}
|
|
209
|
+
return key;
|
|
235
210
|
}
|
|
236
|
-
// src/
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
211
|
+
// src/auth/refresh.ts
|
|
212
|
+
var RE = /^(\d+)([hms])$/;
|
|
213
|
+
function parseDuration(d) {
|
|
214
|
+
const m = RE.exec(d);
|
|
215
|
+
if (!m)
|
|
216
|
+
throw new Error(`Bad duration "${d}" \u2014 expected e.g. "8h", "30m", "45s"`);
|
|
217
|
+
const n = Number(m[1]);
|
|
218
|
+
const unit = m[2];
|
|
219
|
+
if (unit === "h")
|
|
220
|
+
return n * 3600 * 1000;
|
|
221
|
+
if (unit === "m")
|
|
222
|
+
return n * 60 * 1000;
|
|
223
|
+
return n * 1000;
|
|
224
|
+
}
|
|
225
|
+
function needsRefresh(entry, policy, now = new Date) {
|
|
226
|
+
if (!entry)
|
|
252
227
|
return true;
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
228
|
+
const ttlMs = parseDuration(policy.ttl);
|
|
229
|
+
const bufMs = parseDuration(policy.refreshBuffer);
|
|
230
|
+
const createdAt = new Date(entry.created_at).getTime();
|
|
231
|
+
if (now.getTime() - createdAt > ttlMs)
|
|
232
|
+
return true;
|
|
233
|
+
const expiresAt = new Date(entry.expires_at).getTime();
|
|
234
|
+
if (expiresAt - now.getTime() < bufMs)
|
|
235
|
+
return true;
|
|
236
|
+
return false;
|
|
256
237
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
238
|
+
// src/auth/state.ts
|
|
239
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
240
|
+
import { join as join2 } from "path";
|
|
241
|
+
import { z as z3 } from "zod";
|
|
242
|
+
var AuthStateEntrySchema = z3.object({
|
|
243
|
+
role: z3.string(),
|
|
244
|
+
strategy: z3.enum(["storageState", "apiToken"]),
|
|
245
|
+
created_at: z3.string(),
|
|
246
|
+
expires_at: z3.string(),
|
|
247
|
+
payload: z3.record(z3.string(), z3.unknown())
|
|
248
|
+
});
|
|
249
|
+
function pathFor(authDir, role) {
|
|
250
|
+
return join2(authDir, `${role}.json`);
|
|
260
251
|
}
|
|
261
|
-
function
|
|
262
|
-
|
|
252
|
+
function writeAuthState(authDir, entry) {
|
|
253
|
+
mkdirSync3(authDir, { recursive: true });
|
|
254
|
+
const ct = encrypt(JSON.stringify(entry), resolveAuthKey());
|
|
255
|
+
writeFileSync3(pathFor(authDir, entry.role), ct);
|
|
256
|
+
}
|
|
257
|
+
function readAuthState(authDir, role) {
|
|
258
|
+
const p = pathFor(authDir, role);
|
|
259
|
+
if (!existsSync4(p))
|
|
263
260
|
return null;
|
|
264
|
-
|
|
261
|
+
const txt = readFileSync4(p, "utf8");
|
|
262
|
+
const plain = decrypt(txt, resolveAuthKey());
|
|
263
|
+
return AuthStateEntrySchema.parse(JSON.parse(plain));
|
|
265
264
|
}
|
|
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
|
-
}
|
|
265
|
+
// src/config/define.ts
|
|
266
|
+
function defineConfig(config) {
|
|
267
|
+
return config;
|
|
279
268
|
}
|
|
280
|
-
|
|
281
|
-
|
|
269
|
+
// src/config/load.ts
|
|
270
|
+
import { existsSync as existsSync5 } from "fs";
|
|
271
|
+
import { join as join3 } from "path";
|
|
272
|
+
import { pathToFileURL } from "url";
|
|
273
|
+
|
|
274
|
+
// src/config/schema.ts
|
|
275
|
+
import { z as z4 } from "zod";
|
|
276
|
+
var AuthRoleSchema = z4.object({
|
|
277
|
+
envEmail: z4.string().min(1),
|
|
278
|
+
envPassword: z4.string().min(1)
|
|
279
|
+
});
|
|
280
|
+
var AuthSchema = z4.object({
|
|
281
|
+
strategy: z4.enum(["storageState", "apiToken", "none"]).default("none"),
|
|
282
|
+
ttl: z4.string().default("8h"),
|
|
283
|
+
refreshBuffer: z4.string().default("30m"),
|
|
284
|
+
setupScript: z4.string().optional(),
|
|
285
|
+
roles: z4.record(z4.string(), AuthRoleSchema).default({})
|
|
286
|
+
});
|
|
287
|
+
var WebSchema = z4.object({
|
|
288
|
+
baseUrl: z4.record(z4.string(), z4.string().url()).refine((m) => Object.keys(m).length > 0, {
|
|
289
|
+
message: "baseUrl must have at least one environment"
|
|
290
|
+
}),
|
|
291
|
+
defaultEnv: z4.string(),
|
|
292
|
+
auth: AuthSchema.prefault({}),
|
|
293
|
+
testData: z4.object({
|
|
294
|
+
users: z4.record(z4.string(), z4.object({ fromAuth: z4.string() })).default({})
|
|
295
|
+
}).prefault({})
|
|
296
|
+
}).refine((w) => w.baseUrl[w.defaultEnv] !== undefined, {
|
|
297
|
+
message: "defaultEnv must exist in baseUrl map",
|
|
298
|
+
path: ["defaultEnv"]
|
|
299
|
+
});
|
|
300
|
+
var JiraSchema = z4.object({
|
|
301
|
+
baseUrl: z4.string().url(),
|
|
302
|
+
projectKeys: z4.array(z4.string().min(1)).min(1),
|
|
303
|
+
fields: z4.object({
|
|
304
|
+
story: z4.string().min(1),
|
|
305
|
+
acceptanceCriteria: z4.string().optional(),
|
|
306
|
+
attachments: z4.string().default("attachment")
|
|
307
|
+
})
|
|
308
|
+
});
|
|
309
|
+
var AISchema = z4.object({
|
|
310
|
+
livePageSnapshot: z4.boolean().default(true),
|
|
311
|
+
confidenceThreshold: z4.enum(["low", "medium", "high"]).default("medium"),
|
|
312
|
+
maxRetries: z4.object({
|
|
313
|
+
typecheck: z4.number().int().min(0).max(5).default(2),
|
|
314
|
+
lint: z4.number().int().min(0).max(5).default(2),
|
|
315
|
+
validateFeature: z4.number().int().min(0).max(5).default(2)
|
|
316
|
+
}).prefault({})
|
|
317
|
+
}).prefault({});
|
|
318
|
+
var ReportingSchema = z4.object({
|
|
319
|
+
language: z4.enum(["en", "vi"]).default("en"),
|
|
320
|
+
postToJira: z4.boolean().default(true),
|
|
321
|
+
transition: z4.object({
|
|
322
|
+
onPass: z4.string().nullable().default(null),
|
|
323
|
+
onFail: z4.string().nullable().default(null)
|
|
324
|
+
}).prefault({}),
|
|
325
|
+
artifactLinks: z4.enum(["git", "local"]).default("git")
|
|
326
|
+
}).prefault({});
|
|
327
|
+
var XeraConfigSchema = z4.object({
|
|
328
|
+
jira: JiraSchema,
|
|
329
|
+
web: WebSchema,
|
|
330
|
+
ai: AISchema,
|
|
331
|
+
reporting: ReportingSchema,
|
|
332
|
+
adapters: z4.array(z4.string().min(1)).min(1).default(["web"])
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// src/config/load.ts
|
|
336
|
+
async function loadConfig(cwd) {
|
|
337
|
+
const path = join3(cwd, "xera.config.ts");
|
|
338
|
+
if (!existsSync5(path)) {
|
|
339
|
+
throw new Error(`xera.config.ts not found in ${cwd}`);
|
|
340
|
+
}
|
|
341
|
+
const mod = await import(pathToFileURL(path).href);
|
|
342
|
+
const raw = mod.default ?? mod;
|
|
343
|
+
return XeraConfigSchema.parse(raw);
|
|
282
344
|
}
|
|
283
345
|
// src/jira/mcp-backend.ts
|
|
284
|
-
import { existsSync as
|
|
346
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
285
347
|
import { tmpdir } from "os";
|
|
286
|
-
import { join as
|
|
348
|
+
import { join as join4 } from "path";
|
|
287
349
|
var MCP_ENV = "XERA_MCP_JIRA";
|
|
288
350
|
async function createMcpBackend(_baseUrl) {
|
|
289
351
|
if (process.env[MCP_ENV] !== "1")
|
|
290
352
|
return null;
|
|
291
|
-
const tmpDir =
|
|
292
|
-
|
|
353
|
+
const tmpDir = join4(tmpdir(), "xera-mcp");
|
|
354
|
+
mkdirSync4(tmpDir, { recursive: true });
|
|
293
355
|
return {
|
|
294
356
|
backend: "mcp",
|
|
295
357
|
async fetchTicket(key, _fields) {
|
|
296
|
-
const cachePath =
|
|
297
|
-
if (!
|
|
358
|
+
const cachePath = join4(tmpDir, `${key}.json`);
|
|
359
|
+
if (!existsSync6(cachePath)) {
|
|
298
360
|
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
361
|
}
|
|
300
|
-
const parsed = JSON.parse(
|
|
362
|
+
const parsed = JSON.parse(readFileSync5(cachePath, "utf8"));
|
|
301
363
|
return parsed;
|
|
302
364
|
},
|
|
303
365
|
async postComment(key, body) {
|
|
304
|
-
const outPath =
|
|
366
|
+
const outPath = join4(tmpDir, `${key}.comment.json`);
|
|
305
367
|
writeFileSync4(outPath, JSON.stringify({ key, body }));
|
|
306
368
|
return { id: "mcp-pending" };
|
|
307
369
|
},
|
|
308
370
|
async transitionStatus(key, statusName) {
|
|
309
|
-
const outPath =
|
|
371
|
+
const outPath = join4(tmpDir, `${key}.transition.json`);
|
|
310
372
|
writeFileSync4(outPath, JSON.stringify({ key, statusName }));
|
|
311
373
|
},
|
|
312
374
|
async listFields(_sampleKey) {
|
|
@@ -444,111 +506,77 @@ async function withRetry(fn, opts) {
|
|
|
444
506
|
}
|
|
445
507
|
throw lastErr;
|
|
446
508
|
}
|
|
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).`);
|
|
509
|
+
// src/lock/file-lock.ts
|
|
510
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
|
|
511
|
+
import { hostname } from "os";
|
|
512
|
+
import { dirname as dirname3 } from "path";
|
|
513
|
+
function acquireLock(path, runId) {
|
|
514
|
+
if (existsSync7(path))
|
|
515
|
+
return false;
|
|
516
|
+
mkdirSync5(dirname3(path), { recursive: true });
|
|
517
|
+
const data = {
|
|
518
|
+
pid: process.pid,
|
|
519
|
+
hostname: hostname(),
|
|
520
|
+
started_at: new Date().toISOString(),
|
|
521
|
+
run_id: runId
|
|
522
|
+
};
|
|
523
|
+
try {
|
|
524
|
+
writeFileSync5(path, JSON.stringify(data), { flag: "wx" });
|
|
525
|
+
return true;
|
|
526
|
+
} catch {
|
|
527
|
+
return false;
|
|
496
528
|
}
|
|
497
|
-
return key;
|
|
498
529
|
}
|
|
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);
|
|
530
|
+
function releaseLock(path) {
|
|
531
|
+
if (existsSync7(path))
|
|
532
|
+
unlinkSync(path);
|
|
517
533
|
}
|
|
518
|
-
function
|
|
519
|
-
|
|
520
|
-
if (!existsSync8(p))
|
|
534
|
+
function readLock(path) {
|
|
535
|
+
if (!existsSync7(path))
|
|
521
536
|
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;
|
|
537
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
539
538
|
}
|
|
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)
|
|
539
|
+
function isLockStale(path) {
|
|
540
|
+
const lock = readLock(path);
|
|
541
|
+
if (!lock)
|
|
547
542
|
return true;
|
|
548
|
-
|
|
549
|
-
|
|
543
|
+
if (lock.hostname !== hostname()) {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
try {
|
|
547
|
+
process.kill(lock.pid, 0);
|
|
548
|
+
return false;
|
|
549
|
+
} catch {
|
|
550
550
|
return true;
|
|
551
|
-
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function forceUnlock(path) {
|
|
554
|
+
releaseLock(path);
|
|
555
|
+
}
|
|
556
|
+
// src/logging/ndjson-logger.ts
|
|
557
|
+
import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync7 } from "fs";
|
|
558
|
+
import { dirname as dirname4 } from "path";
|
|
559
|
+
|
|
560
|
+
class NdjsonLogger {
|
|
561
|
+
path;
|
|
562
|
+
constructor(path) {
|
|
563
|
+
this.path = path;
|
|
564
|
+
mkdirSync6(dirname4(path), { recursive: true });
|
|
565
|
+
}
|
|
566
|
+
log(payload) {
|
|
567
|
+
const entry = { ts: new Date().toISOString(), ...payload };
|
|
568
|
+
appendFileSync(this.path, `${JSON.stringify(entry)}
|
|
569
|
+
`);
|
|
570
|
+
}
|
|
571
|
+
static readAll(path) {
|
|
572
|
+
if (!existsSync8(path))
|
|
573
|
+
return [];
|
|
574
|
+
const txt = readFileSync7(path, "utf8").trim();
|
|
575
|
+
if (!txt)
|
|
576
|
+
return [];
|
|
577
|
+
return txt.split(`
|
|
578
|
+
`).map((line) => JSON.parse(line));
|
|
579
|
+
}
|
|
552
580
|
}
|
|
553
581
|
|
|
554
582
|
// src/index.ts
|