@xera-ai/web 0.1.5 → 0.1.6
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/generator/typecheck.d.ts +5 -0
- package/dist/index.js +50 -23
- package/package.json +1 -1
- package/src/generator/typecheck.ts +44 -4
|
@@ -2,4 +2,9 @@ export interface TypecheckResult {
|
|
|
2
2
|
ok: boolean;
|
|
3
3
|
errors: string[];
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* Type-check the ticket's TypeScript files using the project's root tsconfig.
|
|
7
|
+
* Errors are filtered to those whose path contains the ticket directory, so the
|
|
8
|
+
* skill sees only the locally relevant ones.
|
|
9
|
+
*/
|
|
5
10
|
export declare function typecheckTicket(ticketDir: string): Promise<TypecheckResult>;
|
package/dist/index.js
CHANGED
|
@@ -357,18 +357,45 @@ function validateGherkin(content) {
|
|
|
357
357
|
}
|
|
358
358
|
// src/generator/typecheck.ts
|
|
359
359
|
import { spawnSync } from "child_process";
|
|
360
|
+
import { existsSync as existsSync2 } from "fs";
|
|
361
|
+
import { dirname, join as join5 } from "path";
|
|
362
|
+
function findNearestTsconfig(start) {
|
|
363
|
+
let dir = start;
|
|
364
|
+
while (true) {
|
|
365
|
+
const candidate = join5(dir, "tsconfig.json");
|
|
366
|
+
if (existsSync2(candidate))
|
|
367
|
+
return candidate;
|
|
368
|
+
const parent = dirname(dir);
|
|
369
|
+
if (parent === dir)
|
|
370
|
+
return null;
|
|
371
|
+
dir = parent;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
360
374
|
async function typecheckTicket(ticketDir) {
|
|
361
|
-
const
|
|
375
|
+
const tsconfig = findNearestTsconfig(ticketDir);
|
|
376
|
+
if (!tsconfig) {
|
|
377
|
+
return {
|
|
378
|
+
ok: false,
|
|
379
|
+
errors: [
|
|
380
|
+
`No tsconfig.json found walking up from ${ticketDir}. Run \`xera init\` to scaffold one at the project root.`
|
|
381
|
+
]
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
const proc = spawnSync("npx", ["tsc", "--noEmit", "-p", tsconfig], { encoding: "utf8" });
|
|
362
385
|
if (proc.status === 0)
|
|
363
386
|
return { ok: true, errors: [] };
|
|
364
|
-
const out =
|
|
365
|
-
const
|
|
387
|
+
const out = `${proc.stdout ?? ""}${proc.stderr ?? ""}`;
|
|
388
|
+
const allErrors = out.split(`
|
|
366
389
|
`).filter((line) => /error TS\d+/.test(line));
|
|
367
|
-
|
|
390
|
+
const ticketErrors = allErrors.filter((line) => line.includes(ticketDir));
|
|
391
|
+
return {
|
|
392
|
+
ok: false,
|
|
393
|
+
errors: ticketErrors.length > 0 ? ticketErrors : allErrors
|
|
394
|
+
};
|
|
368
395
|
}
|
|
369
396
|
// src/generator/lint.ts
|
|
370
|
-
import { readFileSync as readFileSync3, existsSync as
|
|
371
|
-
import { join as
|
|
397
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3, readdirSync } from "fs";
|
|
398
|
+
import { join as join6 } from "path";
|
|
372
399
|
|
|
373
400
|
// src/generator/selector-rules.ts
|
|
374
401
|
var AUTO_CLASS_RE = /\.(?:Mui|css|ant|chakra|MuiButton)[A-Za-z]*-[A-Za-z0-9_]*-[A-Za-z0-9_]{3,}/;
|
|
@@ -401,11 +428,11 @@ function lintSelectors(source) {
|
|
|
401
428
|
|
|
402
429
|
// src/generator/lint.ts
|
|
403
430
|
function listTsFiles(dir) {
|
|
404
|
-
if (!
|
|
431
|
+
if (!existsSync3(dir))
|
|
405
432
|
return [];
|
|
406
433
|
const out = [];
|
|
407
434
|
for (const name of readdirSync(dir, { withFileTypes: true })) {
|
|
408
|
-
const full =
|
|
435
|
+
const full = join6(dir, name.name);
|
|
409
436
|
if (name.isDirectory())
|
|
410
437
|
out.push(...listTsFiles(full));
|
|
411
438
|
else if (name.name.endsWith(".ts"))
|
|
@@ -425,18 +452,18 @@ async function lintTicket(ticketDir) {
|
|
|
425
452
|
return { ok: warnings.length === 0, warnings };
|
|
426
453
|
}
|
|
427
454
|
// src/generator/pom-scan.ts
|
|
428
|
-
import { existsSync as
|
|
429
|
-
import { join as
|
|
455
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
|
|
456
|
+
import { join as join7 } from "path";
|
|
430
457
|
var CLASS_RE = /export\s+class\s+([A-Z][A-Za-z0-9_]*)/g;
|
|
431
458
|
function scanSharedPoms(repoRoot) {
|
|
432
|
-
const dir =
|
|
433
|
-
if (!
|
|
459
|
+
const dir = join7(repoRoot, "shared", "page-objects");
|
|
460
|
+
if (!existsSync4(dir))
|
|
434
461
|
return [];
|
|
435
462
|
const found = [];
|
|
436
463
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
437
464
|
if (!entry.isFile() || !entry.name.endsWith(".ts"))
|
|
438
465
|
continue;
|
|
439
|
-
const path =
|
|
466
|
+
const path = join7(dir, entry.name);
|
|
440
467
|
const src = readFileSync4(path, "utf8");
|
|
441
468
|
for (const m of src.matchAll(CLASS_RE)) {
|
|
442
469
|
found.push({ className: m[1], absolutePath: path });
|
|
@@ -445,23 +472,23 @@ function scanSharedPoms(repoRoot) {
|
|
|
445
472
|
return found;
|
|
446
473
|
}
|
|
447
474
|
// src/generator/promote.ts
|
|
448
|
-
import { existsSync as
|
|
449
|
-
import { join as
|
|
475
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3, renameSync } from "fs";
|
|
476
|
+
import { join as join8 } from "path";
|
|
450
477
|
async function promotePom(input) {
|
|
451
|
-
const fromDir =
|
|
452
|
-
const toDir =
|
|
478
|
+
const fromDir = join8(input.repoRoot, ".xera", input.ticket, "page-objects");
|
|
479
|
+
const toDir = join8(input.repoRoot, "shared", "page-objects");
|
|
453
480
|
const file = `${input.className}.ts`;
|
|
454
|
-
const fromPath =
|
|
455
|
-
const toPath =
|
|
456
|
-
if (!
|
|
481
|
+
const fromPath = join8(fromDir, file);
|
|
482
|
+
const toPath = join8(toDir, file);
|
|
483
|
+
if (!existsSync5(fromPath)) {
|
|
457
484
|
throw new Error(`POM ${file} not found at ${fromPath}`);
|
|
458
485
|
}
|
|
459
|
-
if (
|
|
486
|
+
if (existsSync5(toPath)) {
|
|
460
487
|
throw new Error(`POM ${file} already exists at ${toPath}. Reconcile manually before promoting.`);
|
|
461
488
|
}
|
|
462
489
|
renameSync(fromPath, toPath);
|
|
463
|
-
const specPath =
|
|
464
|
-
if (
|
|
490
|
+
const specPath = join8(input.repoRoot, ".xera", input.ticket, "spec.ts");
|
|
491
|
+
if (existsSync5(specPath)) {
|
|
465
492
|
const src = readFileSync5(specPath, "utf8");
|
|
466
493
|
const updated = src.replace(new RegExp(`from\\s+['"]\\./page-objects/${input.className}['"]`, "g"), `from '../../shared/page-objects/${input.className}'`);
|
|
467
494
|
writeFileSync3(specPath, updated);
|
package/package.json
CHANGED
|
@@ -1,14 +1,54 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
2
4
|
|
|
3
5
|
export interface TypecheckResult {
|
|
4
6
|
ok: boolean;
|
|
5
7
|
errors: string[];
|
|
6
8
|
}
|
|
7
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Walks up from `start` looking for the first `tsconfig.json`. Returns null if
|
|
12
|
+
* none is found before reaching the filesystem root.
|
|
13
|
+
*/
|
|
14
|
+
function findNearestTsconfig(start: string): string | null {
|
|
15
|
+
let dir = start;
|
|
16
|
+
// eslint-disable-next-line no-constant-condition
|
|
17
|
+
while (true) {
|
|
18
|
+
const candidate = join(dir, 'tsconfig.json');
|
|
19
|
+
if (existsSync(candidate)) return candidate;
|
|
20
|
+
const parent = dirname(dir);
|
|
21
|
+
if (parent === dir) return null;
|
|
22
|
+
dir = parent;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Type-check the ticket's TypeScript files using the project's root tsconfig.
|
|
28
|
+
* Errors are filtered to those whose path contains the ticket directory, so the
|
|
29
|
+
* skill sees only the locally relevant ones.
|
|
30
|
+
*/
|
|
8
31
|
export async function typecheckTicket(ticketDir: string): Promise<TypecheckResult> {
|
|
9
|
-
const
|
|
32
|
+
const tsconfig = findNearestTsconfig(ticketDir);
|
|
33
|
+
if (!tsconfig) {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
errors: [
|
|
37
|
+
`No tsconfig.json found walking up from ${ticketDir}. Run \`xera init\` to scaffold one at the project root.`,
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const proc = spawnSync('npx', ['tsc', '--noEmit', '-p', tsconfig], { encoding: 'utf8' });
|
|
10
43
|
if (proc.status === 0) return { ok: true, errors: [] };
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
44
|
+
|
|
45
|
+
const out = `${proc.stdout ?? ''}${proc.stderr ?? ''}`;
|
|
46
|
+
const allErrors = out.split('\n').filter((line) => /error TS\d+/.test(line));
|
|
47
|
+
// Keep errors that originate inside the ticket dir; if none match, fall back
|
|
48
|
+
// to all errors so callers don't see "ok" for a broken root.
|
|
49
|
+
const ticketErrors = allErrors.filter((line) => line.includes(ticketDir));
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
errors: ticketErrors.length > 0 ? ticketErrors : allErrors,
|
|
53
|
+
};
|
|
14
54
|
}
|