@xera-ai/core 0.1.6 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/internal.js +2039 -725
- package/dist/{adapter → core/src/adapter}/types.d.ts +1 -1
- package/dist/core/src/adapter/types.d.ts.map +1 -0
- package/dist/core/src/artifact/hash.d.ts.map +1 -0
- package/dist/core/src/artifact/meta.d.ts.map +1 -0
- package/dist/core/src/artifact/paths.d.ts.map +1 -0
- package/dist/core/src/artifact/status.d.ts.map +1 -0
- package/dist/core/src/auth/encrypt.d.ts.map +1 -0
- package/dist/core/src/auth/key.d.ts.map +1 -0
- package/dist/core/src/auth/refresh.d.ts.map +1 -0
- package/dist/core/src/auth/state.d.ts.map +1 -0
- package/dist/core/src/bin-internal/doctor.d.ts +5 -0
- package/dist/core/src/bin-internal/doctor.d.ts.map +1 -0
- package/dist/core/src/bin-internal/eval-deterministic.d.ts +5 -0
- package/dist/core/src/bin-internal/eval-deterministic.d.ts.map +1 -0
- package/dist/core/src/bin-internal/eval-prepare.d.ts +7 -0
- package/dist/core/src/bin-internal/eval-prepare.d.ts.map +1 -0
- package/dist/core/src/bin-internal/eval-report.d.ts +5 -0
- package/dist/core/src/bin-internal/eval-report.d.ts.map +1 -0
- package/dist/core/src/bin-internal/exec.d.ts.map +1 -0
- package/dist/core/src/bin-internal/fetch.d.ts.map +1 -0
- package/dist/core/src/bin-internal/heal-prepare.d.ts +19 -0
- package/dist/core/src/bin-internal/heal-prepare.d.ts.map +1 -0
- package/dist/core/src/bin-internal/index.d.ts.map +1 -0
- package/dist/core/src/bin-internal/lint.d.ts.map +1 -0
- package/dist/core/src/bin-internal/normalize.d.ts.map +1 -0
- package/dist/core/src/bin-internal/post.d.ts.map +1 -0
- package/dist/core/src/bin-internal/promote.d.ts.map +1 -0
- package/dist/core/src/bin-internal/report.d.ts.map +1 -0
- package/dist/core/src/bin-internal/status-cmd.d.ts.map +1 -0
- package/dist/core/src/bin-internal/typecheck.d.ts.map +1 -0
- package/dist/core/src/bin-internal/unlock.d.ts.map +1 -0
- package/dist/core/src/bin-internal/validate-feature.d.ts.map +1 -0
- package/dist/core/src/bin-internal/verify-prompts.d.ts +7 -0
- package/dist/core/src/bin-internal/verify-prompts.d.ts.map +1 -0
- package/dist/core/src/classifier/aggregate.d.ts.map +1 -0
- package/dist/core/src/classifier/history.d.ts.map +1 -0
- package/dist/core/src/classifier/types.d.ts.map +1 -0
- package/dist/core/src/config/define.d.ts.map +1 -0
- package/dist/core/src/config/load.d.ts.map +1 -0
- package/dist/{config → core/src/config}/schema.d.ts.map +1 -1
- package/dist/core/src/eval/paths.d.ts +15 -0
- package/dist/core/src/eval/paths.d.ts.map +1 -0
- package/dist/core/src/eval/run-id.d.ts +6 -0
- package/dist/core/src/eval/run-id.d.ts.map +1 -0
- package/dist/core/src/eval/types.d.ts +551 -0
- package/dist/core/src/eval/types.d.ts.map +1 -0
- package/dist/core/src/index.d.ts.map +1 -0
- package/dist/core/src/jira/client.d.ts.map +1 -0
- package/dist/core/src/jira/fields.d.ts.map +1 -0
- package/dist/core/src/jira/mcp-backend.d.ts.map +1 -0
- package/dist/core/src/jira/rest-backend.d.ts.map +1 -0
- package/dist/core/src/jira/retry.d.ts.map +1 -0
- package/dist/core/src/jira/types.d.ts.map +1 -0
- package/dist/core/src/lock/file-lock.d.ts.map +1 -0
- package/dist/core/src/logging/ndjson-logger.d.ts.map +1 -0
- package/dist/core/src/reporter/jira-comment.d.ts.map +1 -0
- package/dist/core/src/reporter/status-writer.d.ts.map +1 -0
- package/dist/src/index.js +19 -12
- package/dist/web/src/adapter.d.ts +3 -0
- package/dist/web/src/adapter.d.ts.map +1 -0
- package/dist/web/src/auth-setup/define.d.ts +16 -0
- package/dist/web/src/auth-setup/define.d.ts.map +1 -0
- package/dist/web/src/auth-setup/playwright-state.d.ts +2 -0
- package/dist/web/src/auth-setup/playwright-state.d.ts.map +1 -0
- package/dist/web/src/auth-setup/runner.d.ts +12 -0
- package/dist/web/src/auth-setup/runner.d.ts.map +1 -0
- package/dist/web/src/executor/index.d.ts +18 -0
- package/dist/web/src/executor/index.d.ts.map +1 -0
- package/dist/web/src/executor/playwright-args.d.ts +7 -0
- package/dist/web/src/executor/playwright-args.d.ts.map +1 -0
- package/dist/web/src/generator/gherkin-validate.d.ts +9 -0
- package/dist/web/src/generator/gherkin-validate.d.ts.map +1 -0
- package/dist/web/src/generator/lint.d.ts +9 -0
- package/dist/web/src/generator/lint.d.ts.map +1 -0
- package/dist/web/src/generator/pom-scan.d.ts +6 -0
- package/dist/web/src/generator/pom-scan.d.ts.map +1 -0
- package/dist/web/src/generator/promote.d.ts +7 -0
- package/dist/web/src/generator/promote.d.ts.map +1 -0
- package/dist/web/src/generator/selector-rules.d.ts +10 -0
- package/dist/web/src/generator/selector-rules.d.ts.map +1 -0
- package/dist/web/src/generator/typecheck.d.ts +11 -0
- package/dist/web/src/generator/typecheck.d.ts.map +1 -0
- package/dist/web/src/index.d.ts +18 -0
- package/dist/web/src/index.d.ts.map +1 -0
- package/dist/web/src/trace-normalizer/normalize.d.ts +7 -0
- package/dist/web/src/trace-normalizer/normalize.d.ts.map +1 -0
- package/dist/web/src/trace-normalizer/parse.d.ts +37 -0
- package/dist/web/src/trace-normalizer/parse.d.ts.map +1 -0
- package/dist/web/src/trace-normalizer/scrub-rules.d.ts +12 -0
- package/dist/web/src/trace-normalizer/scrub-rules.d.ts.map +1 -0
- package/dist/web/src/trace-normalizer/scrub.d.ts +29 -0
- package/dist/web/src/trace-normalizer/scrub.d.ts.map +1 -0
- package/dist/web/src/trace-normalizer/unzip.d.ts +6 -0
- package/dist/web/src/trace-normalizer/unzip.d.ts.map +1 -0
- package/package.json +3 -2
- package/src/adapter/types.ts +5 -2
- package/src/artifact/meta.ts +1 -1
- package/src/artifact/status.ts +1 -1
- package/src/auth/encrypt.ts +2 -2
- package/src/auth/key.ts +1 -2
- package/src/auth/refresh.ts +4 -1
- package/src/auth/state.ts +2 -2
- package/src/bin-internal/doctor.ts +133 -0
- package/src/bin-internal/eval-deterministic.ts +149 -0
- package/src/bin-internal/eval-prepare.ts +214 -0
- package/src/bin-internal/eval-report.ts +177 -0
- package/src/bin-internal/exec.ts +38 -16
- package/src/bin-internal/fetch.ts +21 -10
- package/src/bin-internal/heal-prepare.ts +230 -0
- package/src/bin-internal/index.ts +25 -11
- package/src/bin-internal/lint.ts +11 -4
- package/src/bin-internal/normalize.ts +23 -9
- package/src/bin-internal/post.ts +10 -4
- package/src/bin-internal/report.ts +3 -3
- package/src/bin-internal/status-cmd.ts +11 -3
- package/src/bin-internal/typecheck.ts +9 -3
- package/src/bin-internal/unlock.ts +12 -4
- package/src/bin-internal/validate-feature.ts +14 -5
- package/src/bin-internal/verify-prompts.ts +59 -0
- package/src/classifier/aggregate.ts +13 -6
- package/src/config/define.ts +3 -1
- package/src/config/load.ts +1 -1
- package/src/config/schema.ts +43 -37
- package/src/eval/paths.ts +32 -0
- package/src/eval/run-id.ts +30 -0
- package/src/eval/types.ts +101 -0
- package/src/jira/client.ts +4 -2
- package/src/jira/fields.ts +4 -2
- package/src/jira/mcp-backend.ts +1 -1
- package/src/jira/rest-backend.ts +17 -5
- package/src/jira/retry.ts +2 -2
- package/src/lock/file-lock.ts +2 -2
- package/src/logging/ndjson-logger.ts +2 -2
- package/src/reporter/jira-comment.ts +13 -7
- package/src/reporter/status-writer.ts +2 -2
- package/dist/adapter/types.d.ts.map +0 -1
- package/dist/artifact/hash.d.ts.map +0 -1
- package/dist/artifact/meta.d.ts.map +0 -1
- package/dist/artifact/paths.d.ts.map +0 -1
- package/dist/artifact/status.d.ts.map +0 -1
- package/dist/auth/encrypt.d.ts.map +0 -1
- package/dist/auth/key.d.ts.map +0 -1
- package/dist/auth/refresh.d.ts.map +0 -1
- package/dist/auth/state.d.ts.map +0 -1
- package/dist/bin-internal/exec.d.ts.map +0 -1
- package/dist/bin-internal/fetch.d.ts.map +0 -1
- package/dist/bin-internal/index.d.ts.map +0 -1
- package/dist/bin-internal/lint.d.ts.map +0 -1
- package/dist/bin-internal/normalize.d.ts.map +0 -1
- package/dist/bin-internal/post.d.ts.map +0 -1
- package/dist/bin-internal/promote.d.ts.map +0 -1
- package/dist/bin-internal/report.d.ts.map +0 -1
- package/dist/bin-internal/status-cmd.d.ts.map +0 -1
- package/dist/bin-internal/typecheck.d.ts.map +0 -1
- package/dist/bin-internal/unlock.d.ts.map +0 -1
- package/dist/bin-internal/validate-feature.d.ts.map +0 -1
- package/dist/classifier/aggregate.d.ts.map +0 -1
- package/dist/classifier/history.d.ts.map +0 -1
- package/dist/classifier/types.d.ts.map +0 -1
- package/dist/config/define.d.ts.map +0 -1
- package/dist/config/load.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/jira/client.d.ts.map +0 -1
- package/dist/jira/fields.d.ts.map +0 -1
- package/dist/jira/mcp-backend.d.ts.map +0 -1
- package/dist/jira/rest-backend.d.ts.map +0 -1
- package/dist/jira/retry.d.ts.map +0 -1
- package/dist/jira/types.d.ts.map +0 -1
- package/dist/lock/file-lock.d.ts.map +0 -1
- package/dist/logging/ndjson-logger.d.ts.map +0 -1
- package/dist/reporter/jira-comment.d.ts.map +0 -1
- package/dist/reporter/status-writer.d.ts.map +0 -1
- /package/dist/{artifact → core/src/artifact}/hash.d.ts +0 -0
- /package/dist/{artifact → core/src/artifact}/meta.d.ts +0 -0
- /package/dist/{artifact → core/src/artifact}/paths.d.ts +0 -0
- /package/dist/{artifact → core/src/artifact}/status.d.ts +0 -0
- /package/dist/{auth → core/src/auth}/encrypt.d.ts +0 -0
- /package/dist/{auth → core/src/auth}/key.d.ts +0 -0
- /package/dist/{auth → core/src/auth}/refresh.d.ts +0 -0
- /package/dist/{auth → core/src/auth}/state.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/exec.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/fetch.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/index.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/lint.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/normalize.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/post.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/promote.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/report.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/status-cmd.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/typecheck.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/unlock.d.ts +0 -0
- /package/dist/{bin-internal → core/src/bin-internal}/validate-feature.d.ts +0 -0
- /package/dist/{classifier → core/src/classifier}/aggregate.d.ts +0 -0
- /package/dist/{classifier → core/src/classifier}/history.d.ts +0 -0
- /package/dist/{classifier → core/src/classifier}/types.d.ts +0 -0
- /package/dist/{config → core/src/config}/define.d.ts +0 -0
- /package/dist/{config → core/src/config}/load.d.ts +0 -0
- /package/dist/{config → core/src/config}/schema.d.ts +0 -0
- /package/dist/{index.d.ts → core/src/index.d.ts} +0 -0
- /package/dist/{jira → core/src/jira}/client.d.ts +0 -0
- /package/dist/{jira → core/src/jira}/fields.d.ts +0 -0
- /package/dist/{jira → core/src/jira}/mcp-backend.d.ts +0 -0
- /package/dist/{jira → core/src/jira}/rest-backend.d.ts +0 -0
- /package/dist/{jira → core/src/jira}/retry.d.ts +0 -0
- /package/dist/{jira → core/src/jira}/types.d.ts +0 -0
- /package/dist/{lock → core/src/lock}/file-lock.d.ts +0 -0
- /package/dist/{logging → core/src/logging}/ndjson-logger.d.ts +0 -0
- /package/dist/{reporter → core/src/reporter}/jira-comment.d.ts +0 -0
- /package/dist/{reporter → core/src/reporter}/status-writer.d.ts +0 -0
package/src/bin-internal/exec.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { acquireLock, releaseLock, isLockStale, readLock, forceUnlock } from '../lock/file-lock';
|
|
3
|
-
import { NdjsonLogger } from '../logging/ndjson-logger';
|
|
4
|
-
import { loadConfig } from '../config/load';
|
|
5
|
-
import { readAuthState } from '../auth/state';
|
|
6
|
-
import { needsRefresh } from '../auth/refresh';
|
|
7
|
-
import { stagePlaywrightState, runAuthSetup, runPlaywright } from '@xera-ai/web';
|
|
8
|
-
import { chromium } from '@playwright/test';
|
|
9
|
-
import { mkdirSync, existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
10
2
|
import { join } from 'node:path';
|
|
3
|
+
import { chromium } from '@playwright/test';
|
|
4
|
+
import { runAuthSetup, runPlaywright, stagePlaywrightState } from '@xera-ai/web';
|
|
5
|
+
import { generateRunId, resolveArtifactPaths } from '../artifact/paths';
|
|
6
|
+
import { needsRefresh } from '../auth/refresh';
|
|
7
|
+
import { readAuthState } from '../auth/state';
|
|
8
|
+
import { loadConfig } from '../config/load';
|
|
9
|
+
import { acquireLock, forceUnlock, isLockStale, readLock, releaseLock } from '../lock/file-lock';
|
|
10
|
+
import { NdjsonLogger } from '../logging/ndjson-logger';
|
|
11
11
|
|
|
12
12
|
export async function execCmd(argv: string[]): Promise<number> {
|
|
13
13
|
const ticket = argv[0];
|
|
14
|
-
if (!ticket) {
|
|
14
|
+
if (!ticket) {
|
|
15
|
+
console.error('[xera:exec] usage: exec <TICKET>');
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
15
18
|
const cwd = process.cwd();
|
|
16
19
|
const config = await loadConfig(cwd);
|
|
17
20
|
const paths = resolveArtifactPaths(cwd, ticket);
|
|
@@ -21,12 +24,16 @@ export async function execCmd(argv: string[]): Promise<number> {
|
|
|
21
24
|
// Acquire lock
|
|
22
25
|
if (!acquireLock(paths.lockPath, runId)) {
|
|
23
26
|
if (isLockStale(paths.lockPath)) {
|
|
24
|
-
console.error(
|
|
27
|
+
console.error(
|
|
28
|
+
`[xera:exec] stale lock detected; force unlocking. Run \`xera-internal unlock ${ticket}\` to clear manually.`,
|
|
29
|
+
);
|
|
25
30
|
forceUnlock(paths.lockPath);
|
|
26
31
|
acquireLock(paths.lockPath, runId);
|
|
27
32
|
} else {
|
|
28
33
|
const existing = readLock(paths.lockPath);
|
|
29
|
-
console.error(
|
|
34
|
+
console.error(
|
|
35
|
+
`[xera:exec] another run in progress (PID ${existing?.pid} on ${existing?.hostname}, started ${existing?.started_at}). Wait or run \`xera-internal unlock ${ticket}\`.`,
|
|
36
|
+
);
|
|
30
37
|
return 1;
|
|
31
38
|
}
|
|
32
39
|
}
|
|
@@ -39,11 +46,18 @@ export async function execCmd(argv: string[]): Promise<number> {
|
|
|
39
46
|
try {
|
|
40
47
|
for (const [roleName, roleCreds] of Object.entries(config.web.auth.roles)) {
|
|
41
48
|
const entry = readAuthState(paths.authDir, roleName);
|
|
42
|
-
if (
|
|
49
|
+
if (
|
|
50
|
+
needsRefresh(entry, {
|
|
51
|
+
ttl: config.web.auth.ttl,
|
|
52
|
+
refreshBuffer: config.web.auth.refreshBuffer,
|
|
53
|
+
})
|
|
54
|
+
) {
|
|
43
55
|
const email = process.env[roleCreds.envEmail];
|
|
44
56
|
const password = process.env[roleCreds.envPassword];
|
|
45
57
|
if (!email || !password) {
|
|
46
|
-
console.error(
|
|
58
|
+
console.error(
|
|
59
|
+
`[xera:exec] missing env ${roleCreds.envEmail} or ${roleCreds.envPassword} for role "${roleName}"`,
|
|
60
|
+
);
|
|
47
61
|
return 1;
|
|
48
62
|
}
|
|
49
63
|
await runAuthSetup({
|
|
@@ -88,12 +102,21 @@ export async function execCmd(argv: string[]): Promise<number> {
|
|
|
88
102
|
const envName = process.env.XERA_ENV ?? config.web.defaultEnv;
|
|
89
103
|
const baseURL = config.web.baseUrl[envName] ?? config.web.baseUrl[config.web.defaultEnv]!;
|
|
90
104
|
|
|
105
|
+
const reportJsonPath = join(runDir, 'report.json');
|
|
106
|
+
|
|
91
107
|
log.log({ step: 'exec.start', runId, env: envName, baseURL });
|
|
92
108
|
const r = await runPlaywright({
|
|
93
109
|
specPath: paths.specPath,
|
|
94
110
|
configPath: cfgPath,
|
|
95
111
|
outputDir: runDir,
|
|
96
|
-
env: {
|
|
112
|
+
env: {
|
|
113
|
+
XERA_BASE_URL: baseURL,
|
|
114
|
+
XERA_ENV: envName,
|
|
115
|
+
// Playwright's JSON reporter prints to stdout by default. Redirect it
|
|
116
|
+
// to a file inside the run dir so xera:normalize has a deterministic
|
|
117
|
+
// path to read.
|
|
118
|
+
PLAYWRIGHT_JSON_OUTPUT_NAME: reportJsonPath,
|
|
119
|
+
},
|
|
97
120
|
});
|
|
98
121
|
log.log({ step: 'exec.done', runId, exit: r.exitCode, ms: Date.now() - t0 });
|
|
99
122
|
|
|
@@ -104,4 +127,3 @@ export async function execCmd(argv: string[]): Promise<number> {
|
|
|
104
127
|
releaseLock(paths.lockPath);
|
|
105
128
|
}
|
|
106
129
|
}
|
|
107
|
-
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
-
import { loadConfig } from '../config/load';
|
|
4
|
-
import { resolveArtifactPaths } from '../artifact/paths';
|
|
5
3
|
import { hashString } from '../artifact/hash';
|
|
6
|
-
import {
|
|
4
|
+
import { readMeta, writeMeta } from '../artifact/meta';
|
|
5
|
+
import { resolveArtifactPaths } from '../artifact/paths';
|
|
6
|
+
import { loadConfig } from '../config/load';
|
|
7
7
|
import { createJiraClient } from '../jira/client';
|
|
8
8
|
import type { JiraTicket } from '../jira/types';
|
|
9
9
|
|
|
10
|
-
export interface FetchCmdOpts {
|
|
10
|
+
export interface FetchCmdOpts {
|
|
11
|
+
cwd?: string;
|
|
12
|
+
}
|
|
11
13
|
|
|
12
14
|
export async function fetchCmd(argv: string[], opts: FetchCmdOpts = {}): Promise<number> {
|
|
13
15
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -31,9 +33,13 @@ export async function fetchCmd(argv: string[], opts: FetchCmdOpts = {}): Promise
|
|
|
31
33
|
? { rest: { email: process.env.JIRA_EMAIL, apiToken: process.env.JIRA_API_TOKEN } }
|
|
32
34
|
: {}),
|
|
33
35
|
});
|
|
34
|
-
const fieldMap =
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
const fieldMap =
|
|
37
|
+
config.jira.fields.acceptanceCriteria !== undefined
|
|
38
|
+
? {
|
|
39
|
+
story: config.jira.fields.story,
|
|
40
|
+
acceptanceCriteria: config.jira.fields.acceptanceCriteria,
|
|
41
|
+
}
|
|
42
|
+
: { story: config.jira.fields.story };
|
|
37
43
|
t = await client.fetchTicket(ticket, fieldMap);
|
|
38
44
|
}
|
|
39
45
|
|
|
@@ -69,7 +75,7 @@ function renderStory(t: JiraTicket): string {
|
|
|
69
75
|
lines.push('## Story', '', story, '');
|
|
70
76
|
}
|
|
71
77
|
|
|
72
|
-
if (t.acceptanceCriteria
|
|
78
|
+
if (t.acceptanceCriteria?.trim()) {
|
|
73
79
|
const ac = t.acceptanceCriteria.trim();
|
|
74
80
|
if (/^##\s+acceptance\s+criteria\b/i.test(ac)) {
|
|
75
81
|
lines.push(ac, '');
|
|
@@ -78,7 +84,12 @@ function renderStory(t: JiraTicket): string {
|
|
|
78
84
|
}
|
|
79
85
|
}
|
|
80
86
|
if (t.attachments.length > 0) {
|
|
81
|
-
lines.push(
|
|
87
|
+
lines.push(
|
|
88
|
+
'## Attachments',
|
|
89
|
+
'',
|
|
90
|
+
...t.attachments.map((a) => `- [${a.filename}](${a.url})`),
|
|
91
|
+
'',
|
|
92
|
+
);
|
|
82
93
|
}
|
|
83
94
|
return lines.join('\n');
|
|
84
95
|
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { scrubFreeText } from '@xera-ai/web';
|
|
4
|
+
import { unzipSync } from 'fflate';
|
|
5
|
+
import { resolveArtifactPaths } from '../artifact/paths';
|
|
6
|
+
|
|
7
|
+
export type FailedLocatorKind = 'role' | 'test-id' | 'css-class' | 'text' | 'label' | 'other';
|
|
8
|
+
|
|
9
|
+
export interface HealInput {
|
|
10
|
+
ticket: string;
|
|
11
|
+
runId: string;
|
|
12
|
+
scenarioName: string;
|
|
13
|
+
failedLocator: {
|
|
14
|
+
raw: string;
|
|
15
|
+
kind: FailedLocatorKind;
|
|
16
|
+
pomFile: string;
|
|
17
|
+
pomLine: number;
|
|
18
|
+
pomLineContent: string;
|
|
19
|
+
pomMethodName: string;
|
|
20
|
+
};
|
|
21
|
+
gherkinStep: string;
|
|
22
|
+
domSnapshotAtFailure: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ClassifierInput {
|
|
26
|
+
runId: string;
|
|
27
|
+
scenarios: Array<{
|
|
28
|
+
name: string;
|
|
29
|
+
outcome: string;
|
|
30
|
+
class: string;
|
|
31
|
+
confidence: string;
|
|
32
|
+
rationale: string;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface NormalizedRunFile {
|
|
37
|
+
runId: string;
|
|
38
|
+
scenarios: Array<{
|
|
39
|
+
name: string;
|
|
40
|
+
outcome: 'PASS' | 'FAIL' | 'SKIPPED';
|
|
41
|
+
failure?: { errorMessage?: string };
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const LOCATOR_LINE_RE = /^Locator:\s*(.+)$/m;
|
|
46
|
+
|
|
47
|
+
function classifyKind(raw: string): FailedLocatorKind {
|
|
48
|
+
if (/^getByRole\b/.test(raw)) return 'role';
|
|
49
|
+
if (/^getByTestId\b/.test(raw)) return 'test-id';
|
|
50
|
+
if (/^getByLabel\b/.test(raw)) return 'label';
|
|
51
|
+
if (/^getByText\b/.test(raw)) return 'text';
|
|
52
|
+
if (/^locator\(\s*['"`]\s*\.[A-Za-z_-]/.test(raw)) return 'css-class';
|
|
53
|
+
return 'other';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function extractDomSnapshot(tracePath: string): string {
|
|
57
|
+
if (!existsSync(tracePath)) return '';
|
|
58
|
+
const buf = readFileSync(tracePath);
|
|
59
|
+
const entries = unzipSync(buf);
|
|
60
|
+
|
|
61
|
+
// Strategy: parse the .trace JSONL event file to find the last frame-snapshot
|
|
62
|
+
// event, then extract the HTML resource it references. This gives us the DOM
|
|
63
|
+
// snapshot closest to the failure point rather than a lexicographic approximation.
|
|
64
|
+
// Falls back to last .html by lex sort if the .trace file is missing or unparseable.
|
|
65
|
+
const traceKey = Object.keys(entries).find((name) => name.endsWith('.trace'));
|
|
66
|
+
let chosenKey: string | null = null;
|
|
67
|
+
|
|
68
|
+
if (traceKey) {
|
|
69
|
+
const traceText = new TextDecoder().decode(entries[traceKey]!);
|
|
70
|
+
const lines = traceText.split('\n').filter(Boolean);
|
|
71
|
+
// Walk events in REVERSE order to find the most recent frame-snapshot.
|
|
72
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
73
|
+
try {
|
|
74
|
+
const evt = JSON.parse(lines[i]!);
|
|
75
|
+
const isSnapshot = evt.type === 'frame-snapshot' || evt.type === 'snapshot';
|
|
76
|
+
if (!isSnapshot) continue;
|
|
77
|
+
const snap = evt.snapshot ?? {};
|
|
78
|
+
// Try direct resource reference (older format).
|
|
79
|
+
const resourceName: unknown = snap.resourceName;
|
|
80
|
+
if (typeof resourceName === 'string') {
|
|
81
|
+
if (entries[resourceName]) {
|
|
82
|
+
chosenKey = resourceName;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
// Some traces store .html files under resources/ with the resourceName as the basename.
|
|
86
|
+
const guessed = `resources/${resourceName.replace(/^resources\//, '')}`;
|
|
87
|
+
if (entries[guessed]) {
|
|
88
|
+
chosenKey = guessed;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Try snapshot name → look for matching .html in resources/.
|
|
93
|
+
const snapshotName: unknown = snap.snapshotName;
|
|
94
|
+
if (typeof snapshotName === 'string') {
|
|
95
|
+
const directGuess = Object.keys(entries).find(
|
|
96
|
+
(k) => k.endsWith('.html') && k.includes(snapshotName),
|
|
97
|
+
);
|
|
98
|
+
if (directGuess) {
|
|
99
|
+
chosenKey = directGuess;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
// Skip unparseable trace lines.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Fallback: last .html by lexicographic sort (existing heuristic).
|
|
110
|
+
if (!chosenKey) {
|
|
111
|
+
const htmlKeys = Object.keys(entries)
|
|
112
|
+
.filter((name) => name.endsWith('.html'))
|
|
113
|
+
.sort();
|
|
114
|
+
chosenKey = htmlKeys[htmlKeys.length - 1] ?? null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!chosenKey) return '';
|
|
118
|
+
const html = new TextDecoder().decode(entries[chosenKey]!);
|
|
119
|
+
return scrubFreeText(html);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function findPomLine(
|
|
123
|
+
ticketDir: string,
|
|
124
|
+
rawLocator: string,
|
|
125
|
+
): { pomFile: string; pomLine: number; pomLineContent: string; pomMethodName: string } {
|
|
126
|
+
const pomDir = join(ticketDir, 'page-objects');
|
|
127
|
+
const candidates: string[] = [];
|
|
128
|
+
if (existsSync(pomDir)) {
|
|
129
|
+
for (const name of readdirSync(pomDir)) {
|
|
130
|
+
if (name.endsWith('.ts')) candidates.push(join(pomDir, name));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
for (const file of candidates) {
|
|
134
|
+
const text = readFileSync(file, 'utf8');
|
|
135
|
+
const lines = text.split('\n');
|
|
136
|
+
for (let i = 0; i < lines.length; i++) {
|
|
137
|
+
const line = lines[i]!;
|
|
138
|
+
if (line.includes(rawLocator)) {
|
|
139
|
+
const methodMatch = /^\s*(\w+)\s*=/.exec(line);
|
|
140
|
+
return {
|
|
141
|
+
pomFile: file,
|
|
142
|
+
pomLine: i + 1,
|
|
143
|
+
pomLineContent: line,
|
|
144
|
+
pomMethodName: methodMatch?.[1] ?? '<anonymous>',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
throw new Error(`POM line not found for locator: ${rawLocator}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function findGherkinStep(featureText: string, rawLocator: string): string {
|
|
153
|
+
// Best-effort: find the first step line that mentions a quoted string
|
|
154
|
+
// appearing in the locator (e.g. a button name). Falls back to the
|
|
155
|
+
// first When/Then line if no match.
|
|
156
|
+
const quoteMatch = /['"`]([^'"`]{2,})['"`]/.exec(rawLocator);
|
|
157
|
+
if (quoteMatch) {
|
|
158
|
+
const needle = quoteMatch[1]!;
|
|
159
|
+
for (const line of featureText.split('\n')) {
|
|
160
|
+
if (line.includes(needle) && /^\s*(When|Then|And|Given)\b/.test(line)) {
|
|
161
|
+
return line.trim();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const line of featureText.split('\n')) {
|
|
166
|
+
if (/^\s*(When|Then)\b/.test(line)) return line.trim();
|
|
167
|
+
}
|
|
168
|
+
return '';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function healPrepare(
|
|
172
|
+
repoRoot: string,
|
|
173
|
+
ticket: string,
|
|
174
|
+
runId: string,
|
|
175
|
+
scenarioName: string,
|
|
176
|
+
): HealInput {
|
|
177
|
+
const paths = resolveArtifactPaths(repoRoot, ticket);
|
|
178
|
+
const classifierPath = join(paths.ticketDir, 'classifier-input.json');
|
|
179
|
+
const classifier: ClassifierInput = JSON.parse(readFileSync(classifierPath, 'utf8'));
|
|
180
|
+
const cls = classifier.scenarios.find((s) => s.name === scenarioName);
|
|
181
|
+
if (!cls) throw new Error(`scenario not found in classifier-input: "${scenarioName}"`);
|
|
182
|
+
|
|
183
|
+
const runDir = join(paths.runsDir, runId);
|
|
184
|
+
const normalized: NormalizedRunFile = JSON.parse(
|
|
185
|
+
readFileSync(join(runDir, 'normalized.json'), 'utf8'),
|
|
186
|
+
);
|
|
187
|
+
const normSc = normalized.scenarios.find((s) => s.name === scenarioName);
|
|
188
|
+
if (!normSc?.failure) throw new Error(`no failure recorded for scenario "${scenarioName}"`);
|
|
189
|
+
const errorMessage = normSc.failure.errorMessage ?? '';
|
|
190
|
+
const m = LOCATOR_LINE_RE.exec(errorMessage);
|
|
191
|
+
if (!m) throw new Error(`cannot extract locator from errorMessage: ${errorMessage.slice(0, 80)}`);
|
|
192
|
+
const raw = m[1]!.trim();
|
|
193
|
+
const kind = classifyKind(raw);
|
|
194
|
+
|
|
195
|
+
const pomLoc = findPomLine(paths.ticketDir, raw);
|
|
196
|
+
|
|
197
|
+
const featureText = readFileSync(paths.featurePath, 'utf8');
|
|
198
|
+
const gherkinStep = findGherkinStep(featureText, raw);
|
|
199
|
+
|
|
200
|
+
const domSnapshotAtFailure = extractDomSnapshot(join(runDir, 'trace.zip'));
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
ticket,
|
|
204
|
+
runId,
|
|
205
|
+
scenarioName,
|
|
206
|
+
failedLocator: { raw, kind, ...pomLoc },
|
|
207
|
+
gherkinStep,
|
|
208
|
+
domSnapshotAtFailure,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function healPrepareCmd(argv: string[]): Promise<number> {
|
|
213
|
+
const [ticket, runId, ...scenarioParts] = argv;
|
|
214
|
+
if (!ticket || !runId || scenarioParts.length === 0) {
|
|
215
|
+
console.error('[xera:heal-prepare] usage: heal-prepare <TICKET> <RUN_ID> <SCENARIO_NAME>');
|
|
216
|
+
return 1;
|
|
217
|
+
}
|
|
218
|
+
const scenarioName = scenarioParts.join(' ');
|
|
219
|
+
try {
|
|
220
|
+
const result = healPrepare(process.cwd(), ticket, runId, scenarioName);
|
|
221
|
+
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
222
|
+
const outPath = join(paths.runsDir, runId, 'heal-input.json');
|
|
223
|
+
writeFileSync(outPath, JSON.stringify(result, null, 2));
|
|
224
|
+
console.log(`[xera:heal-prepare] wrote ${outPath}`);
|
|
225
|
+
return 0;
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.error(`[xera:heal-prepare] ${(err as Error).message}`);
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -1,33 +1,47 @@
|
|
|
1
|
+
import { doctorCmd } from './doctor';
|
|
2
|
+
import { evalDeterministicCmd } from './eval-deterministic';
|
|
3
|
+
import { evalPrepareCmd } from './eval-prepare';
|
|
4
|
+
import { evalReportCmd } from './eval-report';
|
|
5
|
+
import { execCmd } from './exec';
|
|
1
6
|
import { fetchCmd } from './fetch';
|
|
2
|
-
import {
|
|
3
|
-
import { typecheckCmd } from './typecheck';
|
|
7
|
+
import { healPrepareCmd } from './heal-prepare';
|
|
4
8
|
import { lintCmd } from './lint';
|
|
5
|
-
import { execCmd } from './exec';
|
|
6
9
|
import { normalizeCmd } from './normalize';
|
|
7
|
-
import { reportCmd } from './report';
|
|
8
10
|
import { postCmd } from './post';
|
|
11
|
+
import { promoteCmd } from './promote';
|
|
12
|
+
import { reportCmd } from './report';
|
|
9
13
|
import { statusCmd } from './status-cmd';
|
|
14
|
+
import { typecheckCmd } from './typecheck';
|
|
10
15
|
import { unlockCmd } from './unlock';
|
|
11
|
-
import {
|
|
16
|
+
import { validateFeatureCmd } from './validate-feature';
|
|
17
|
+
import { verifyPromptsCmd } from './verify-prompts';
|
|
12
18
|
|
|
13
19
|
const COMMANDS: Record<string, (argv: string[]) => Promise<number>> = {
|
|
20
|
+
doctor: doctorCmd,
|
|
21
|
+
'eval-deterministic': evalDeterministicCmd,
|
|
22
|
+
'eval-prepare': evalPrepareCmd,
|
|
23
|
+
'eval-report': evalReportCmd,
|
|
24
|
+
exec: execCmd,
|
|
14
25
|
fetch: fetchCmd,
|
|
15
|
-
'
|
|
16
|
-
typecheck: typecheckCmd,
|
|
26
|
+
'heal-prepare': healPrepareCmd,
|
|
17
27
|
lint: lintCmd,
|
|
18
|
-
exec: execCmd,
|
|
19
28
|
normalize: normalizeCmd,
|
|
20
|
-
report: reportCmd,
|
|
21
29
|
post: postCmd,
|
|
30
|
+
promote: promoteCmd,
|
|
31
|
+
report: reportCmd,
|
|
22
32
|
status: statusCmd,
|
|
33
|
+
typecheck: typecheckCmd,
|
|
23
34
|
unlock: unlockCmd,
|
|
24
|
-
|
|
35
|
+
'validate-feature': validateFeatureCmd,
|
|
36
|
+
'verify-prompts': verifyPromptsCmd,
|
|
25
37
|
};
|
|
26
38
|
|
|
27
39
|
export async function run(argv: string[]): Promise<number> {
|
|
28
40
|
const [cmd, ...rest] = argv;
|
|
29
41
|
if (!cmd || !COMMANDS[cmd]) {
|
|
30
|
-
console.error(
|
|
42
|
+
console.error(
|
|
43
|
+
`Usage: xera-internal <command> [args...]\nCommands: ${Object.keys(COMMANDS).join(', ')}`,
|
|
44
|
+
);
|
|
31
45
|
return 1;
|
|
32
46
|
}
|
|
33
47
|
try {
|
package/src/bin-internal/lint.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
import { resolveArtifactPaths } from '../artifact/paths';
|
|
2
1
|
import { lintTicket } from '@xera-ai/web';
|
|
2
|
+
import { resolveArtifactPaths } from '../artifact/paths';
|
|
3
3
|
|
|
4
4
|
export async function lintCmd(argv: string[]): Promise<number> {
|
|
5
5
|
const ticket = argv[0];
|
|
6
|
-
if (!ticket) {
|
|
6
|
+
if (!ticket) {
|
|
7
|
+
console.error('[xera:lint] usage: lint <TICKET>');
|
|
8
|
+
return 1;
|
|
9
|
+
}
|
|
7
10
|
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
8
11
|
const r = await lintTicket(paths.ticketDir);
|
|
9
|
-
if (r.ok) {
|
|
10
|
-
|
|
12
|
+
if (r.ok) {
|
|
13
|
+
console.log('[xera:lint] ok');
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
for (const w of r.warnings)
|
|
17
|
+
console.error(`[xera:lint] ${w.file}:${w.line} [${w.rule}] ${w.message}`);
|
|
11
18
|
return 2;
|
|
12
19
|
}
|
|
@@ -1,20 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { normalizeRun } from '@xera-ai/web';
|
|
3
|
-
import { readdirSync, existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
4
2
|
import { join } from 'node:path';
|
|
3
|
+
import { normalizeRun } from '@xera-ai/web';
|
|
4
|
+
import { resolveArtifactPaths } from '../artifact/paths';
|
|
5
5
|
|
|
6
6
|
export async function normalizeCmd(argv: string[]): Promise<number> {
|
|
7
7
|
const ticket = argv[0];
|
|
8
|
-
if (!ticket) {
|
|
8
|
+
if (!ticket) {
|
|
9
|
+
console.error('[xera:normalize] usage: normalize <TICKET> [--run=<runId>]');
|
|
10
|
+
return 1;
|
|
11
|
+
}
|
|
9
12
|
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
10
|
-
const runArg = argv.find(a => a.startsWith('--run='));
|
|
13
|
+
const runArg = argv.find((a) => a.startsWith('--run='));
|
|
11
14
|
const runId = runArg
|
|
12
15
|
? runArg.split('=')[1]!
|
|
13
|
-
: readdirSync(paths.runsDir)
|
|
14
|
-
|
|
16
|
+
: readdirSync(paths.runsDir)
|
|
17
|
+
.filter((n) => !n.startsWith('.'))
|
|
18
|
+
.sort()
|
|
19
|
+
.pop()!;
|
|
20
|
+
if (!runId) {
|
|
21
|
+
console.error('[xera:normalize] no run found');
|
|
22
|
+
return 1;
|
|
23
|
+
}
|
|
15
24
|
const runDir = join(paths.runsDir, runId);
|
|
16
|
-
if (!existsSync(runDir)) {
|
|
25
|
+
if (!existsSync(runDir)) {
|
|
26
|
+
console.error(`[xera:normalize] runs/${runId} missing`);
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
17
29
|
const r = await normalizeRun({ runId, runDir });
|
|
18
|
-
console.log(
|
|
30
|
+
console.log(
|
|
31
|
+
`[xera:normalize] wrote normalized.json (scrubbed_fields_count=${r.scrubbed_fields_count})`,
|
|
32
|
+
);
|
|
19
33
|
return 0;
|
|
20
34
|
}
|
package/src/bin-internal/post.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { resolveArtifactPaths } from '../artifact/paths';
|
|
4
|
-
import { loadConfig } from '../config/load';
|
|
5
4
|
import { readStatus, writeStatus } from '../artifact/status';
|
|
5
|
+
import { loadConfig } from '../config/load';
|
|
6
6
|
import { createJiraClient } from '../jira/client';
|
|
7
7
|
|
|
8
8
|
export async function postCmd(argv: string[]): Promise<number> {
|
|
9
9
|
const ticket = argv[0];
|
|
10
|
-
if (!ticket) {
|
|
10
|
+
if (!ticket) {
|
|
11
|
+
console.error('[xera:post] usage: post <TICKET>');
|
|
12
|
+
return 1;
|
|
13
|
+
}
|
|
11
14
|
const cwd = process.cwd();
|
|
12
15
|
const config = await loadConfig(cwd);
|
|
13
16
|
if (!config.reporting.postToJira) {
|
|
@@ -16,7 +19,10 @@ export async function postCmd(argv: string[]): Promise<number> {
|
|
|
16
19
|
}
|
|
17
20
|
const paths = resolveArtifactPaths(cwd, ticket);
|
|
18
21
|
const draftPath = join(paths.ticketDir, 'jira-comment.draft.md');
|
|
19
|
-
if (!existsSync(draftPath)) {
|
|
22
|
+
if (!existsSync(draftPath)) {
|
|
23
|
+
console.error(`[xera:post] no draft at ${draftPath}; run \`xera-internal report\` first.`);
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
20
26
|
const body = readFileSync(draftPath, 'utf8');
|
|
21
27
|
|
|
22
28
|
const client = await createJiraClient({
|
|
@@ -2,9 +2,9 @@ import { readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { resolveArtifactPaths } from '../artifact/paths';
|
|
4
4
|
import { aggregateScenarios } from '../classifier/aggregate';
|
|
5
|
-
import { writeStatusFromClassification } from '../reporter/status-writer';
|
|
6
|
-
import { buildJiraComment } from '../reporter/jira-comment';
|
|
7
5
|
import type { ScenarioClassification } from '../classifier/types';
|
|
6
|
+
import { buildJiraComment } from '../reporter/jira-comment';
|
|
7
|
+
import { writeStatusFromClassification } from '../reporter/status-writer';
|
|
8
8
|
|
|
9
9
|
interface ReportInput {
|
|
10
10
|
scenarios: ScenarioClassification[];
|
|
@@ -14,7 +14,7 @@ interface ReportInput {
|
|
|
14
14
|
|
|
15
15
|
export async function reportCmd(argv: string[]): Promise<number> {
|
|
16
16
|
const ticket = argv[0];
|
|
17
|
-
const inputArg = argv.find(a => a.startsWith('--input='));
|
|
17
|
+
const inputArg = argv.find((a) => a.startsWith('--input='));
|
|
18
18
|
if (!ticket || !inputArg) {
|
|
19
19
|
console.error('[xera:report] usage: report <TICKET> --input=<classifier-output.json>');
|
|
20
20
|
return 1;
|
|
@@ -3,11 +3,19 @@ import { readStatus } from '../artifact/status';
|
|
|
3
3
|
|
|
4
4
|
export async function statusCmd(argv: string[]): Promise<number> {
|
|
5
5
|
const ticket = argv[0];
|
|
6
|
-
if (!ticket) {
|
|
6
|
+
if (!ticket) {
|
|
7
|
+
console.error('[xera:status] usage: status <TICKET>');
|
|
8
|
+
return 1;
|
|
9
|
+
}
|
|
7
10
|
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
8
11
|
const s = readStatus(paths.statusPath);
|
|
9
|
-
if (!s) {
|
|
10
|
-
|
|
12
|
+
if (!s) {
|
|
13
|
+
console.log(`[xera:status] no status yet for ${ticket}`);
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
console.log(
|
|
17
|
+
`${ticket}: ${s.result} (${s.classification}, conf=${s.confidence}) — ${s.scenarios.passed}/${s.scenarios.total} passed, last run ${s.lastRun}`,
|
|
18
|
+
);
|
|
11
19
|
for (const h of s.history.slice(0, 5)) console.log(` ${h.ts} ${h.result} ${h.class}`);
|
|
12
20
|
return 0;
|
|
13
21
|
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import { resolveArtifactPaths } from '../artifact/paths';
|
|
2
1
|
import { typecheckTicket } from '@xera-ai/web';
|
|
2
|
+
import { resolveArtifactPaths } from '../artifact/paths';
|
|
3
3
|
|
|
4
4
|
export async function typecheckCmd(argv: string[]): Promise<number> {
|
|
5
5
|
const ticket = argv[0];
|
|
6
|
-
if (!ticket) {
|
|
6
|
+
if (!ticket) {
|
|
7
|
+
console.error('[xera:typecheck] usage: typecheck <TICKET>');
|
|
8
|
+
return 1;
|
|
9
|
+
}
|
|
7
10
|
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
8
11
|
const r = await typecheckTicket(paths.ticketDir);
|
|
9
|
-
if (r.ok) {
|
|
12
|
+
if (r.ok) {
|
|
13
|
+
console.log('[xera:typecheck] ok');
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
10
16
|
for (const e of r.errors) console.error(`[xera:typecheck] ${e}`);
|
|
11
17
|
return 2;
|
|
12
18
|
}
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import { resolveArtifactPaths } from '../artifact/paths';
|
|
2
|
-
import { isLockStale, readLock
|
|
2
|
+
import { forceUnlock, isLockStale, readLock } from '../lock/file-lock';
|
|
3
3
|
|
|
4
4
|
export async function unlockCmd(argv: string[]): Promise<number> {
|
|
5
5
|
const ticket = argv[0];
|
|
6
|
-
if (!ticket) {
|
|
6
|
+
if (!ticket) {
|
|
7
|
+
console.error('[xera:unlock] usage: unlock <TICKET> [--force]');
|
|
8
|
+
return 1;
|
|
9
|
+
}
|
|
7
10
|
const paths = resolveArtifactPaths(process.cwd(), ticket);
|
|
8
11
|
const lock = readLock(paths.lockPath);
|
|
9
|
-
if (!lock) {
|
|
12
|
+
if (!lock) {
|
|
13
|
+
console.log(`[xera:unlock] no lock for ${ticket}`);
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
10
16
|
const force = argv.includes('--force');
|
|
11
17
|
if (!force && !isLockStale(paths.lockPath)) {
|
|
12
|
-
console.error(
|
|
18
|
+
console.error(
|
|
19
|
+
`[xera:unlock] lock is held by PID ${lock.pid} on ${lock.hostname} (active). Pass --force to override.`,
|
|
20
|
+
);
|
|
13
21
|
return 1;
|
|
14
22
|
}
|
|
15
23
|
forceUnlock(paths.lockPath);
|