@tested/cli 0.1.5 → 0.1.7
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/README.md +8 -5
- package/dist/td.js +438 -57
- package/dist/tested.js +438 -57
- package/package.json +1 -1
package/dist/tested.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command11 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/init.ts
|
|
7
7
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "fs";
|
|
8
8
|
import { join } from "path";
|
|
9
|
-
import "commander";
|
|
9
|
+
import { Option } from "commander";
|
|
10
10
|
import { simpleGit } from "simple-git";
|
|
11
11
|
|
|
12
12
|
// src/output/ui.ts
|
|
@@ -115,6 +115,15 @@ function formatCliError(message) {
|
|
|
115
115
|
"or pass --token <token> (avoid on shared hosts: visible in ps)"
|
|
116
116
|
]);
|
|
117
117
|
}
|
|
118
|
+
if (/git base ref .+ not found/i.test(m)) {
|
|
119
|
+
const refMatch = m.match(/git base ref "([^"]+)" not found/i);
|
|
120
|
+
const ref = refMatch?.[1] ?? "the requested ref";
|
|
121
|
+
return errorBlock(`git base ref "${ref}" not found`, [
|
|
122
|
+
"That ref is not in this repository.",
|
|
123
|
+
"Pass --base HEAD~1 (or a branch / commit that exists)",
|
|
124
|
+
"or set `base:` in .tested.yaml"
|
|
125
|
+
]);
|
|
126
|
+
}
|
|
118
127
|
if (/invalid PR number/i.test(m)) {
|
|
119
128
|
return errorBlock("invalid PR number", [
|
|
120
129
|
m.replace(/^invalid PR number\s*/i, "").replace(/^—\s*/, "") || m,
|
|
@@ -209,6 +218,16 @@ var PRE_PUSH_HOOK_BODY = `#!/usr/bin/env sh
|
|
|
209
218
|
# Installed by \`tested init\`. Skip with \`git push --no-verify\`.
|
|
210
219
|
tested diff
|
|
211
220
|
`;
|
|
221
|
+
function resolveInitHooks(opts) {
|
|
222
|
+
if (!opts.hooks) return false;
|
|
223
|
+
const env = opts.env ?? process.env;
|
|
224
|
+
const ci = env.CI === "1" || env.CI === "true" || env.GITHUB_ACTIONS === "true";
|
|
225
|
+
const isTTY = opts.isTTY ?? Boolean(process.stdin.isTTY);
|
|
226
|
+
if ((!isTTY || ci) && !opts.force) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
212
231
|
async function runInit(opts) {
|
|
213
232
|
const { cwd, force, hooks } = opts;
|
|
214
233
|
const pkgJsonPath = join(cwd, "package.json");
|
|
@@ -294,13 +313,13 @@ function formatInitResultHuman(result) {
|
|
|
294
313
|
return lines.join("\n");
|
|
295
314
|
}
|
|
296
315
|
function registerInitCommand(program2) {
|
|
297
|
-
program2.command("init").description("Initialize tested.dev in the current project (writes .tested.yaml)").option("--force", "Overwrite an existing .tested.yaml", false).option("--
|
|
316
|
+
program2.command("init").description("Initialize tested.dev in the current project (writes .tested.yaml)").option("--force", "Overwrite an existing .tested.yaml", false).option("--hooks", "Install husky pre-push hook (skipped by default in CI / non-TTY)", false).addOption(new Option("--no-hooks").hideHelp()).option("--json", "Emit JSON instead of human text", false).action(async (opts) => {
|
|
298
317
|
try {
|
|
299
|
-
if (opts.hooks && !
|
|
318
|
+
if (opts.hooks && !resolveInitHooks({ hooks: true, force: opts.force })) {
|
|
300
319
|
process.stderr.write(
|
|
301
320
|
errorBlock(
|
|
302
321
|
"--hooks in a non-TTY environment requires --force to confirm",
|
|
303
|
-
["Would install a git hook unattended."]
|
|
322
|
+
["Would install a git hook unattended.", "Omit --hooks to init without a hook."]
|
|
304
323
|
)
|
|
305
324
|
);
|
|
306
325
|
process.exit(1);
|
|
@@ -308,7 +327,7 @@ function registerInitCommand(program2) {
|
|
|
308
327
|
const result = await runInit({
|
|
309
328
|
cwd: process.cwd(),
|
|
310
329
|
force: opts.force,
|
|
311
|
-
hooks: opts.hooks
|
|
330
|
+
hooks: resolveInitHooks({ hooks: opts.hooks, force: opts.force })
|
|
312
331
|
});
|
|
313
332
|
if (opts.json) {
|
|
314
333
|
process.stdout.write(JSON.stringify(buildInitJsonOutput(result), null, 2) + "\n");
|
|
@@ -612,17 +631,85 @@ import { resolve as resolve3 } from "path";
|
|
|
612
631
|
|
|
613
632
|
// src/git.ts
|
|
614
633
|
import { simpleGit as simpleGit2 } from "simple-git";
|
|
634
|
+
|
|
635
|
+
// src/git-ref.ts
|
|
636
|
+
var SAFE_GIT_REF_RE = /^[A-Za-z0-9_./@~^-]{1,256}$/;
|
|
637
|
+
function assertSafeGitRef(ref) {
|
|
638
|
+
if (!ref) {
|
|
639
|
+
throw new Error("git ref must not be empty");
|
|
640
|
+
}
|
|
641
|
+
if (ref.startsWith("-")) {
|
|
642
|
+
throw new Error(`git ref must not start with '-': ${ref}`);
|
|
643
|
+
}
|
|
644
|
+
if (!SAFE_GIT_REF_RE.test(ref)) {
|
|
645
|
+
throw new Error(
|
|
646
|
+
`git ref contains invalid characters or is too long (max 256): ${ref}`
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
return ref;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// src/git.ts
|
|
615
653
|
async function openRepo(cwd) {
|
|
616
654
|
const git = simpleGit2({ baseDir: cwd });
|
|
617
655
|
const repoRoot = (await git.revparse(["--show-toplevel"])).trim();
|
|
618
656
|
return { git, repoRoot };
|
|
619
657
|
}
|
|
620
|
-
async function
|
|
621
|
-
|
|
658
|
+
async function tryRevparse(ctx, ref) {
|
|
659
|
+
try {
|
|
660
|
+
const sha = (await ctx.git.revparse([ref])).trim();
|
|
661
|
+
return sha || null;
|
|
662
|
+
} catch {
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function missingGitRefMessage(ref) {
|
|
667
|
+
return `git base ref "${ref}" not found`;
|
|
668
|
+
}
|
|
669
|
+
async function resolveEffectiveBase(ctx, requested) {
|
|
670
|
+
const requestedSha = await tryRevparse(ctx, requested);
|
|
671
|
+
if (!requestedSha) {
|
|
672
|
+
throw new Error(missingGitRefMessage(requested));
|
|
673
|
+
}
|
|
674
|
+
const head = await headSha(ctx);
|
|
675
|
+
if (requestedSha !== head) {
|
|
676
|
+
return { ref: requested, sha: requestedSha };
|
|
677
|
+
}
|
|
678
|
+
const upstream = await tryRevparse(ctx, "@{upstream}");
|
|
679
|
+
if (upstream && upstream !== head) {
|
|
680
|
+
return { ref: "@{upstream}", sha: upstream };
|
|
681
|
+
}
|
|
682
|
+
const parent = await tryRevparse(ctx, "HEAD~1");
|
|
683
|
+
if (parent) {
|
|
684
|
+
return { ref: "HEAD~1", sha: parent };
|
|
685
|
+
}
|
|
686
|
+
return { ref: requested, sha: requestedSha };
|
|
622
687
|
}
|
|
623
688
|
async function headSha(ctx) {
|
|
624
689
|
return (await ctx.git.revparse(["HEAD"])).trim();
|
|
625
690
|
}
|
|
691
|
+
async function fetchOriginRef(ctx, ref) {
|
|
692
|
+
const spec = ref.startsWith("origin/") ? ref.slice("origin/".length) : ref;
|
|
693
|
+
try {
|
|
694
|
+
assertSafeGitRef(spec);
|
|
695
|
+
} catch {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
try {
|
|
699
|
+
await ctx.git.raw(["fetch", "--depth=1", "origin", spec]);
|
|
700
|
+
return true;
|
|
701
|
+
} catch {
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
async function resolveAfterFetch(ctx, ref) {
|
|
706
|
+
if (await tryRevparse(ctx, ref)) return ref;
|
|
707
|
+
if (!ref.startsWith("origin/")) {
|
|
708
|
+
const originRef = `origin/${ref}`;
|
|
709
|
+
if (await tryRevparse(ctx, originRef)) return originRef;
|
|
710
|
+
}
|
|
711
|
+
return tryRevparse(ctx, "FETCH_HEAD");
|
|
712
|
+
}
|
|
626
713
|
async function unifiedDiff(ctx, base) {
|
|
627
714
|
try {
|
|
628
715
|
const mergeBase = (await ctx.git.raw(["merge-base", base, "HEAD"])).trim();
|
|
@@ -653,23 +740,6 @@ async function gitUserName(ctx) {
|
|
|
653
740
|
}
|
|
654
741
|
}
|
|
655
742
|
|
|
656
|
-
// src/git-ref.ts
|
|
657
|
-
var SAFE_GIT_REF_RE = /^[A-Za-z0-9_./@~^-]{1,256}$/;
|
|
658
|
-
function assertSafeGitRef(ref) {
|
|
659
|
-
if (!ref) {
|
|
660
|
-
throw new Error("git ref must not be empty");
|
|
661
|
-
}
|
|
662
|
-
if (ref.startsWith("-")) {
|
|
663
|
-
throw new Error(`git ref must not start with '-': ${ref}`);
|
|
664
|
-
}
|
|
665
|
-
if (!SAFE_GIT_REF_RE.test(ref)) {
|
|
666
|
-
throw new Error(
|
|
667
|
-
`git ref contains invalid characters or is too long (max 256): ${ref}`
|
|
668
|
-
);
|
|
669
|
-
}
|
|
670
|
-
return ref;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
743
|
// src/core/istanbul.ts
|
|
674
744
|
import { readFile as readFile2 } from "fs/promises";
|
|
675
745
|
import { isAbsolute, relative, resolve } from "path";
|
|
@@ -914,8 +984,8 @@ function buildDiffOutput(args) {
|
|
|
914
984
|
async function computeDiff(opts) {
|
|
915
985
|
const { cwd, config } = opts;
|
|
916
986
|
const ctx = opts.ctx ?? await openRepo(cwd);
|
|
917
|
-
const
|
|
918
|
-
const base = await
|
|
987
|
+
const requested = assertSafeGitRef(opts.baseRef ?? config.base);
|
|
988
|
+
const { ref: baseRef, sha: base } = await resolveEffectiveBase(ctx, requested);
|
|
919
989
|
const head = await headSha(ctx);
|
|
920
990
|
const diffText = await unifiedDiff(ctx, base);
|
|
921
991
|
const addedByFile = parseUnifiedDiff(diffText);
|
|
@@ -1124,6 +1194,125 @@ function sanitizeAuthor(name) {
|
|
|
1124
1194
|
function toBranchName(ref) {
|
|
1125
1195
|
return ref.replace(/^refs\/heads\//, "").replace(/^refs\/remotes\//, "").replace(/^origin\//, "");
|
|
1126
1196
|
}
|
|
1197
|
+
var GITHUB_COMMIT_SHA_RE = /^[0-9a-f]{40,64}$/i;
|
|
1198
|
+
function githubApiToken(env) {
|
|
1199
|
+
const raw = env.GITHUB_TOKEN ?? env.GH_TOKEN;
|
|
1200
|
+
if (raw === void 0 || raw === "") return null;
|
|
1201
|
+
return raw;
|
|
1202
|
+
}
|
|
1203
|
+
function parseGitHubPullBase(parsed) {
|
|
1204
|
+
if (!parsed || typeof parsed !== "object" || !("base" in parsed)) return null;
|
|
1205
|
+
const base = parsed.base;
|
|
1206
|
+
if (!base || typeof base !== "object") return null;
|
|
1207
|
+
const ref = "ref" in base ? base.ref : void 0;
|
|
1208
|
+
const sha = "sha" in base ? base.sha : void 0;
|
|
1209
|
+
if (typeof ref !== "string" || typeof sha !== "string") return null;
|
|
1210
|
+
if (!GITHUB_COMMIT_SHA_RE.test(sha)) return null;
|
|
1211
|
+
try {
|
|
1212
|
+
return { ref: assertSafeGitRef(ref), sha: assertSafeGitRef(sha) };
|
|
1213
|
+
} catch {
|
|
1214
|
+
return null;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
async function fetchGitHubPullBase(opts) {
|
|
1218
|
+
if (!parseGitHubRepository(`${opts.owner}/${opts.name}`)) return null;
|
|
1219
|
+
if (!Number.isInteger(opts.prNumber) || opts.prNumber <= 0) return null;
|
|
1220
|
+
const fetchFn = opts.fetchFn ?? globalThis.fetch;
|
|
1221
|
+
const url = `https://api.github.com/repos/${opts.owner}/${opts.name}/pulls/${opts.prNumber}`;
|
|
1222
|
+
const headers = {
|
|
1223
|
+
Accept: "application/vnd.github+json",
|
|
1224
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
1225
|
+
"User-Agent": "tested-cli"
|
|
1226
|
+
};
|
|
1227
|
+
const token = githubApiToken(opts.env ?? process.env);
|
|
1228
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
1229
|
+
let res;
|
|
1230
|
+
try {
|
|
1231
|
+
res = await fetchFn(url, { method: "GET", redirect: "manual", headers });
|
|
1232
|
+
} catch {
|
|
1233
|
+
return null;
|
|
1234
|
+
}
|
|
1235
|
+
if (res.status !== 200) return null;
|
|
1236
|
+
try {
|
|
1237
|
+
return parseGitHubPullBase(JSON.parse(await res.text()));
|
|
1238
|
+
} catch {
|
|
1239
|
+
return null;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
async function resolvePrPushBase(opts) {
|
|
1243
|
+
let requested;
|
|
1244
|
+
try {
|
|
1245
|
+
requested = assertSafeGitRef(opts.requested);
|
|
1246
|
+
} catch {
|
|
1247
|
+
return void 0;
|
|
1248
|
+
}
|
|
1249
|
+
if (await tryRevparse(opts.ctx, requested)) return requested;
|
|
1250
|
+
const branch = toBranchName(requested) || "main";
|
|
1251
|
+
const originRef = `origin/${branch}`;
|
|
1252
|
+
if (originRef !== requested && await tryRevparse(opts.ctx, originRef)) {
|
|
1253
|
+
return originRef;
|
|
1254
|
+
}
|
|
1255
|
+
let prBase = null;
|
|
1256
|
+
if (opts.owner && opts.name) {
|
|
1257
|
+
prBase = await fetchGitHubPullBase({
|
|
1258
|
+
owner: opts.owner,
|
|
1259
|
+
name: opts.name,
|
|
1260
|
+
prNumber: opts.prNumber,
|
|
1261
|
+
...opts.fetchFn ? { fetchFn: opts.fetchFn } : {},
|
|
1262
|
+
...opts.env ? { env: opts.env } : {}
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
if (prBase && await tryRevparse(opts.ctx, prBase.sha)) return prBase.sha;
|
|
1266
|
+
const fetchTargets = [];
|
|
1267
|
+
if (prBase) {
|
|
1268
|
+
fetchTargets.push(prBase.sha, prBase.ref);
|
|
1269
|
+
}
|
|
1270
|
+
fetchTargets.push(branch);
|
|
1271
|
+
let announced = false;
|
|
1272
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1273
|
+
for (const target of fetchTargets) {
|
|
1274
|
+
if (seen.has(target)) continue;
|
|
1275
|
+
seen.add(target);
|
|
1276
|
+
let spec;
|
|
1277
|
+
try {
|
|
1278
|
+
spec = assertSafeGitRef(target);
|
|
1279
|
+
} catch {
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
if (!announced) {
|
|
1283
|
+
announced = true;
|
|
1284
|
+
opts.onProgress?.("fetching base\u2026");
|
|
1285
|
+
}
|
|
1286
|
+
if (!await fetchOriginRef(opts.ctx, spec)) continue;
|
|
1287
|
+
const resolved = await resolveAfterFetch(opts.ctx, spec);
|
|
1288
|
+
if (resolved) return resolved;
|
|
1289
|
+
if (prBase && await tryRevparse(opts.ctx, prBase.sha)) return prBase.sha;
|
|
1290
|
+
}
|
|
1291
|
+
return void 0;
|
|
1292
|
+
}
|
|
1293
|
+
async function peekRepoIdentity(opts) {
|
|
1294
|
+
let owner = opts.owner;
|
|
1295
|
+
let name = opts.name;
|
|
1296
|
+
if (!owner || !name) {
|
|
1297
|
+
const fromActions = parseGitHubRepository(opts.env.GITHUB_REPOSITORY);
|
|
1298
|
+
if (fromActions) {
|
|
1299
|
+
owner = owner ?? fromActions.owner;
|
|
1300
|
+
name = name ?? fromActions.name;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
if (!owner || !name) {
|
|
1304
|
+
try {
|
|
1305
|
+
const origin = await remoteUrl(opts.ctx, "origin");
|
|
1306
|
+
const parsed = parseGitHubRemote(origin);
|
|
1307
|
+
if (parsed) {
|
|
1308
|
+
owner = owner ?? parsed.owner;
|
|
1309
|
+
name = name ?? parsed.name;
|
|
1310
|
+
}
|
|
1311
|
+
} catch {
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
return { owner: owner ?? null, name: name ?? null };
|
|
1315
|
+
}
|
|
1127
1316
|
function buildIngestBody(input) {
|
|
1128
1317
|
const baseRefName = toBranchName(input.baseRef);
|
|
1129
1318
|
return {
|
|
@@ -1316,16 +1505,16 @@ function formatMissingTokenError(opts) {
|
|
|
1316
1505
|
"or pass --token <token> (avoid on shared hosts: visible in ps)"
|
|
1317
1506
|
]);
|
|
1318
1507
|
}
|
|
1319
|
-
function formatPushError(status, message, code) {
|
|
1508
|
+
function formatPushError(status, message, code, identity) {
|
|
1320
1509
|
if (status === 0) {
|
|
1321
1510
|
return errorBlock(message);
|
|
1322
1511
|
}
|
|
1323
1512
|
const normalized = (code ?? message).toLowerCase();
|
|
1324
|
-
if (normalized.includes("token_required") || normalized.includes("invalid token") || normalized.includes("unauthorized") || status === 401) {
|
|
1513
|
+
if (normalized.includes("token_required") || normalized.includes("invalid token") || normalized.includes("unauthorized") || normalized.includes("invalid credentials") || status === 401) {
|
|
1325
1514
|
return errorBlock("ingest auth failed", [
|
|
1326
1515
|
message,
|
|
1327
1516
|
"",
|
|
1328
|
-
...tokenMintGuidance(),
|
|
1517
|
+
...tokenMintGuidance(identity),
|
|
1329
1518
|
"or --token"
|
|
1330
1519
|
]);
|
|
1331
1520
|
}
|
|
@@ -1414,21 +1603,41 @@ async function executePush(cli, deps) {
|
|
|
1414
1603
|
}
|
|
1415
1604
|
const config = await loadConfigFn({ cwd: deps.cwd });
|
|
1416
1605
|
const ctx = await openRepoFn(deps.cwd);
|
|
1417
|
-
|
|
1606
|
+
const peeked = await peekRepoIdentity({
|
|
1607
|
+
...cli.owner !== void 0 ? { owner: cli.owner } : {},
|
|
1608
|
+
...cli.name !== void 0 ? { name: cli.name } : {},
|
|
1609
|
+
env,
|
|
1610
|
+
ctx
|
|
1611
|
+
});
|
|
1418
1612
|
let diff;
|
|
1419
1613
|
try {
|
|
1614
|
+
let baseOverride = cli.base;
|
|
1615
|
+
if (baseOverride === void 0 && prNumber !== null) {
|
|
1616
|
+
const resolved = await resolvePrPushBase({
|
|
1617
|
+
ctx,
|
|
1618
|
+
requested: config.base,
|
|
1619
|
+
prNumber,
|
|
1620
|
+
...peeked.owner != null ? { owner: peeked.owner } : {},
|
|
1621
|
+
...peeked.name != null ? { name: peeked.name } : {},
|
|
1622
|
+
fetchFn,
|
|
1623
|
+
env,
|
|
1624
|
+
onProgress
|
|
1625
|
+
});
|
|
1626
|
+
if (resolved !== void 0) baseOverride = resolved;
|
|
1627
|
+
}
|
|
1628
|
+
onProgress("computing diff\u2026");
|
|
1420
1629
|
diff = await computeDiffFn({
|
|
1421
1630
|
cwd: deps.cwd,
|
|
1422
1631
|
config,
|
|
1423
|
-
...
|
|
1632
|
+
...baseOverride !== void 0 ? { baseRef: baseOverride } : {},
|
|
1424
1633
|
ctx
|
|
1425
1634
|
});
|
|
1426
1635
|
} catch (err) {
|
|
1427
1636
|
const message = err instanceof Error ? err.message : String(err);
|
|
1428
1637
|
return { exitCode: 1, stdout: "", stderr: errorBlock(message) };
|
|
1429
1638
|
}
|
|
1430
|
-
let owner = cli.owner;
|
|
1431
|
-
let name = cli.name;
|
|
1639
|
+
let owner = cli.owner ?? peeked.owner ?? void 0;
|
|
1640
|
+
let name = cli.name ?? peeked.name ?? void 0;
|
|
1432
1641
|
if (!owner || !name) {
|
|
1433
1642
|
const fromActions = parseGitHubRepository(env.GITHUB_REPOSITORY);
|
|
1434
1643
|
if (fromActions) {
|
|
@@ -1516,7 +1725,10 @@ async function executePush(cli, deps) {
|
|
|
1516
1725
|
return {
|
|
1517
1726
|
exitCode: 1,
|
|
1518
1727
|
stdout: "",
|
|
1519
|
-
stderr: formatPushError(result.status, result.message, result.code
|
|
1728
|
+
stderr: formatPushError(result.status, result.message, result.code, {
|
|
1729
|
+
owner,
|
|
1730
|
+
name
|
|
1731
|
+
})
|
|
1520
1732
|
};
|
|
1521
1733
|
}
|
|
1522
1734
|
const formatted = formatPushSuccess(result.data, cli.json);
|
|
@@ -1915,7 +2127,7 @@ import pc3 from "picocolors";
|
|
|
1915
2127
|
// package.json
|
|
1916
2128
|
var package_default = {
|
|
1917
2129
|
name: "@tested/cli",
|
|
1918
|
-
version: "0.1.
|
|
2130
|
+
version: "0.1.7",
|
|
1919
2131
|
description: "Coverage your agent can use. CLI for patch + project coverage with agent-readable JSON output.",
|
|
1920
2132
|
license: "MIT",
|
|
1921
2133
|
homepage: "https://tested.dev",
|
|
@@ -2160,6 +2372,26 @@ import { existsSync as existsSync5 } from "fs";
|
|
|
2160
2372
|
import { isAbsolute as isAbsolute3, resolve as resolve5, sep as sep2 } from "path";
|
|
2161
2373
|
import { spawn } from "child_process";
|
|
2162
2374
|
import "commander";
|
|
2375
|
+
function splitRunArgs(extraArgs) {
|
|
2376
|
+
let json = false;
|
|
2377
|
+
const forwarded = [];
|
|
2378
|
+
let passthrough = false;
|
|
2379
|
+
for (const a of extraArgs) {
|
|
2380
|
+
if (!passthrough && a === "--") {
|
|
2381
|
+
passthrough = true;
|
|
2382
|
+
continue;
|
|
2383
|
+
}
|
|
2384
|
+
if (!passthrough && (a === "--json" || a === "--json=true")) {
|
|
2385
|
+
json = true;
|
|
2386
|
+
continue;
|
|
2387
|
+
}
|
|
2388
|
+
forwarded.push(a);
|
|
2389
|
+
}
|
|
2390
|
+
return { json, forwarded };
|
|
2391
|
+
}
|
|
2392
|
+
function buildRunJsonOutput(input) {
|
|
2393
|
+
return { schemaVersion: 1, ...input };
|
|
2394
|
+
}
|
|
2163
2395
|
function resolveRunCommand(opts) {
|
|
2164
2396
|
const runner = opts.runner ?? "vitest";
|
|
2165
2397
|
switch (runner) {
|
|
@@ -2242,12 +2474,15 @@ function assertSafeRunArgs(extraArgs, repoRoot) {
|
|
|
2242
2474
|
}
|
|
2243
2475
|
function registerRunCommand(program2) {
|
|
2244
2476
|
program2.command("run").description(
|
|
2245
|
-
"Run the
|
|
2246
|
-
).allowUnknownOption(true).argument("[args...]", "Extra arguments forwarded to the runner").action(async (extraArgs) => {
|
|
2477
|
+
"Run the project test suite with coverage (writes coverage even if tests fail)"
|
|
2478
|
+
).option("--json", "Emit tested JSON summary (not forwarded to the runner)", false).allowUnknownOption(true).argument("[args...]", "Extra arguments forwarded to the runner").action(async (extraArgs, opts) => {
|
|
2247
2479
|
const cwd = process.cwd();
|
|
2480
|
+
const split = splitRunArgs(extraArgs ?? []);
|
|
2481
|
+
const json = Boolean(opts.json) || split.json;
|
|
2482
|
+
const forwarded = split.forwarded;
|
|
2248
2483
|
try {
|
|
2249
2484
|
if (shouldEnforceSafeRun()) {
|
|
2250
|
-
assertSafeRunArgs(
|
|
2485
|
+
assertSafeRunArgs(forwarded, cwd);
|
|
2251
2486
|
}
|
|
2252
2487
|
} catch (err) {
|
|
2253
2488
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2259,14 +2494,28 @@ function registerRunCommand(program2) {
|
|
|
2259
2494
|
const coveragePath = resolve5(cwd, config.coverage.path);
|
|
2260
2495
|
const { command, args } = resolveRunCommand({
|
|
2261
2496
|
runner: config.testRunner,
|
|
2262
|
-
extraArgs
|
|
2497
|
+
extraArgs: forwarded
|
|
2263
2498
|
});
|
|
2264
|
-
|
|
2265
|
-
|
|
2499
|
+
if (!json) {
|
|
2500
|
+
process.stderr.write(heading("tested.dev \u2014 running tests with coverage") + "\n");
|
|
2501
|
+
process.stderr.write(dim(`${command} ${args.join(" ")}`) + "\n\n");
|
|
2502
|
+
}
|
|
2266
2503
|
const child = spawn(command, args, { stdio: "inherit" });
|
|
2267
2504
|
child.on("exit", (code) => {
|
|
2268
2505
|
const exit = code ?? 1;
|
|
2269
2506
|
const coverageWritten = existsSync5(coveragePath);
|
|
2507
|
+
if (json) {
|
|
2508
|
+
const payload = buildRunJsonOutput({
|
|
2509
|
+
command,
|
|
2510
|
+
args,
|
|
2511
|
+
exitCode: exit,
|
|
2512
|
+
coverageWritten,
|
|
2513
|
+
coveragePath: config.coverage.path
|
|
2514
|
+
});
|
|
2515
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
2516
|
+
process.exit(exit);
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2270
2519
|
if (exit === 0) {
|
|
2271
2520
|
process.stderr.write("\n");
|
|
2272
2521
|
process.stderr.write(tip("tested diff") + "\n");
|
|
@@ -2381,24 +2630,21 @@ function formatHuman(out, opts = {}) {
|
|
|
2381
2630
|
);
|
|
2382
2631
|
}
|
|
2383
2632
|
}
|
|
2633
|
+
const patchFiles = out.files.filter((f) => f.patchCoverage !== null);
|
|
2384
2634
|
if (isEmptyPatch(out.patch)) {
|
|
2385
2635
|
lines.push("");
|
|
2386
2636
|
lines.push(dim("No executable lines in the patch \u2014 patch gate does not apply."));
|
|
2387
|
-
} else if (
|
|
2637
|
+
} else if (patchFiles.length > 0) {
|
|
2388
2638
|
lines.push("");
|
|
2389
2639
|
lines.push(heading("Files in diff:"));
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
for (const f of out.files) {
|
|
2395
|
-
const hasPatch = f.patchCoverage !== null;
|
|
2396
|
-
const pctPart = hasPatch ? coloredPctCell(f.patchCoverage) : coloredPctCell(f.projectCoverage);
|
|
2397
|
-
lines.push(` ${pctPart} ${f.path}`);
|
|
2640
|
+
for (const f of patchFiles) {
|
|
2641
|
+
const pct3 = f.patchCoverage;
|
|
2642
|
+
if (pct3 === null) continue;
|
|
2643
|
+
lines.push(` ${coloredPctCell(pct3)} ${f.path}`);
|
|
2398
2644
|
if (f.uncoveredRanges.length > 0) {
|
|
2399
2645
|
const ranges = formatRangeList(f.uncoveredRanges);
|
|
2400
2646
|
lines.push(dim(` uncovered: ${ranges}`));
|
|
2401
|
-
} else
|
|
2647
|
+
} else {
|
|
2402
2648
|
lines.push(dim(" fully covered in patch"));
|
|
2403
2649
|
}
|
|
2404
2650
|
}
|
|
@@ -2682,17 +2928,150 @@ function formatIgnoresList(patterns, asJson) {
|
|
|
2682
2928
|
if (asJson) return JSON.stringify({ ignores: [...patterns] });
|
|
2683
2929
|
return patterns.join("\n");
|
|
2684
2930
|
}
|
|
2931
|
+
async function printIgnores(asJson) {
|
|
2932
|
+
const config = await loadConfig({ cwd: process.cwd() });
|
|
2933
|
+
process.stdout.write(formatIgnoresList(config.ignores, asJson) + "\n");
|
|
2934
|
+
}
|
|
2685
2935
|
function registerIgnoresCommand(program2) {
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2936
|
+
program2.command("ignores").description("List ignore patterns (defaults + user). `list` is optional.").argument("[subcommand]", 'optional "list" (the default)').option("--json", "Emit JSON", false).action(async (subcommand, opts) => {
|
|
2937
|
+
if (subcommand !== void 0 && subcommand !== "list") {
|
|
2938
|
+
throw new Error(`unknown ignores subcommand "${subcommand}". Try: tested ignores list`);
|
|
2939
|
+
}
|
|
2940
|
+
await printIgnores(opts.json);
|
|
2941
|
+
});
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
// src/commands/token.ts
|
|
2945
|
+
import "commander";
|
|
2946
|
+
async function resolveRepoIdentity(opts) {
|
|
2947
|
+
try {
|
|
2948
|
+
const open = opts.openRepoFn ?? openRepo;
|
|
2949
|
+
const getUrl = opts.remoteUrlFn ?? remoteUrl;
|
|
2950
|
+
const ctx = await open(opts.cwd);
|
|
2951
|
+
const url = await getUrl(ctx, "origin");
|
|
2952
|
+
const parsed = parseGitHubRemote(url);
|
|
2953
|
+
return parsed ?? { owner: null, name: null };
|
|
2954
|
+
} catch {
|
|
2955
|
+
return { owner: null, name: null };
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
function tokenSourceFromEnv(env) {
|
|
2959
|
+
if (env.TESTED_TOKEN) return "TESTED_TOKEN";
|
|
2960
|
+
if (env.TESTED_INGEST_TOKEN) return "TESTED_INGEST_TOKEN";
|
|
2961
|
+
if (env.TESTED_TOKEN_FILE) return "TESTED_TOKEN_FILE";
|
|
2962
|
+
return "token";
|
|
2963
|
+
}
|
|
2964
|
+
function formatTokenHuman(identity) {
|
|
2965
|
+
const lines = [
|
|
2966
|
+
heading("tested.dev \u2014 token"),
|
|
2967
|
+
"",
|
|
2968
|
+
...tokenMintGuidance(identity).map((line) => dim(` ${line}`)),
|
|
2969
|
+
""
|
|
2970
|
+
];
|
|
2971
|
+
return lines.join("\n");
|
|
2972
|
+
}
|
|
2973
|
+
function formatTokenJson(identity) {
|
|
2974
|
+
return JSON.stringify(
|
|
2975
|
+
{
|
|
2976
|
+
schemaVersion: 1,
|
|
2977
|
+
mintUrl: ingestTokenSettingsUrl(identity.owner, identity.name),
|
|
2978
|
+
envNames: [...INGEST_TOKEN_ENV_NAMES],
|
|
2979
|
+
owner: identity.owner,
|
|
2980
|
+
name: identity.name
|
|
2981
|
+
},
|
|
2982
|
+
null,
|
|
2983
|
+
2
|
|
2984
|
+
) + "\n";
|
|
2985
|
+
}
|
|
2986
|
+
function formatWhoamiHuman(result) {
|
|
2987
|
+
const lines = [heading("tested.dev \u2014 whoami"), ""];
|
|
2988
|
+
if (result.tokenSet) {
|
|
2989
|
+
lines.push(dim(` token: set via ${result.source} (value not shown)`));
|
|
2990
|
+
} else {
|
|
2991
|
+
lines.push(dim(" token: not set"));
|
|
2992
|
+
for (const line of tokenMintGuidance(result.identity)) {
|
|
2993
|
+
lines.push(dim(` ${line}`));
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
lines.push("");
|
|
2997
|
+
return lines.join("\n");
|
|
2998
|
+
}
|
|
2999
|
+
function formatWhoamiJson(result) {
|
|
3000
|
+
return JSON.stringify(
|
|
3001
|
+
{
|
|
3002
|
+
schemaVersion: 1,
|
|
3003
|
+
tokenSet: result.tokenSet,
|
|
3004
|
+
source: result.source,
|
|
3005
|
+
mintUrl: ingestTokenSettingsUrl(result.identity.owner, result.identity.name),
|
|
3006
|
+
envNames: [...INGEST_TOKEN_ENV_NAMES]
|
|
3007
|
+
},
|
|
3008
|
+
null,
|
|
3009
|
+
2
|
|
3010
|
+
) + "\n";
|
|
3011
|
+
}
|
|
3012
|
+
async function runToken(opts) {
|
|
3013
|
+
const identity = await resolveRepoIdentity(opts);
|
|
3014
|
+
const stdout = opts.json ? formatTokenJson(identity) : formatTokenHuman(identity);
|
|
3015
|
+
return { stdout, exitCode: 0 };
|
|
3016
|
+
}
|
|
3017
|
+
async function runWhoami(opts) {
|
|
3018
|
+
const env = opts.env ?? process.env;
|
|
3019
|
+
const identity = await resolveRepoIdentity(opts);
|
|
3020
|
+
const resolve7 = opts.resolveTokenFn ?? resolveToken;
|
|
3021
|
+
let tokenSet = false;
|
|
3022
|
+
let source = null;
|
|
3023
|
+
try {
|
|
3024
|
+
const token = resolve7({ env, isTTY: false, warn: () => {
|
|
3025
|
+
} });
|
|
3026
|
+
tokenSet = Boolean(token);
|
|
3027
|
+
source = tokenSet ? tokenSourceFromEnv(env) : null;
|
|
3028
|
+
} catch (err) {
|
|
3029
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3030
|
+
return {
|
|
3031
|
+
stdout: "",
|
|
3032
|
+
stderr: formatCliError(message),
|
|
3033
|
+
exitCode: 1
|
|
3034
|
+
};
|
|
3035
|
+
}
|
|
3036
|
+
const result = { tokenSet, source, identity, exitCode: tokenSet ? 0 : 1 };
|
|
3037
|
+
const stdout = opts.json ? formatWhoamiJson(result) : formatWhoamiHuman(result);
|
|
3038
|
+
return { stdout, stderr: "", exitCode: result.exitCode };
|
|
3039
|
+
}
|
|
3040
|
+
function registerTokenCommand(program2) {
|
|
3041
|
+
program2.command("token").description("Print the ingest-token mint URL and accepted env names").option("--json", "Emit machine-readable JSON", false).action(async (opts) => {
|
|
3042
|
+
try {
|
|
3043
|
+
const result = await runToken({ cwd: process.cwd(), json: opts.json });
|
|
3044
|
+
process.stdout.write(result.stdout.endsWith("\n") ? result.stdout : result.stdout + "\n");
|
|
3045
|
+
process.exitCode = result.exitCode;
|
|
3046
|
+
} catch (err) {
|
|
3047
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3048
|
+
process.stderr.write(formatCliError(message));
|
|
3049
|
+
process.exitCode = 1;
|
|
3050
|
+
}
|
|
3051
|
+
});
|
|
3052
|
+
}
|
|
3053
|
+
function registerWhoamiCommand(program2) {
|
|
3054
|
+
program2.command("whoami").description("Report whether an ingest token is set (never prints the value)").option("--json", "Emit machine-readable JSON", false).action(async (opts) => {
|
|
3055
|
+
try {
|
|
3056
|
+
const result = await runWhoami({ cwd: process.cwd(), json: opts.json });
|
|
3057
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
3058
|
+
if (result.stdout) {
|
|
3059
|
+
process.stdout.write(
|
|
3060
|
+
result.stdout.endsWith("\n") ? result.stdout : result.stdout + "\n"
|
|
3061
|
+
);
|
|
3062
|
+
}
|
|
3063
|
+
process.exitCode = result.exitCode;
|
|
3064
|
+
} catch (err) {
|
|
3065
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3066
|
+
process.stderr.write(errorBlock(message));
|
|
3067
|
+
process.exitCode = 1;
|
|
3068
|
+
}
|
|
2690
3069
|
});
|
|
2691
3070
|
}
|
|
2692
3071
|
|
|
2693
3072
|
// src/cli.ts
|
|
2694
3073
|
function createProgram() {
|
|
2695
|
-
const program2 = new
|
|
3074
|
+
const program2 = new Command11();
|
|
2696
3075
|
program2.name("tested").description(
|
|
2697
3076
|
[
|
|
2698
3077
|
"Coverage your agent can use.",
|
|
@@ -2708,6 +3087,8 @@ function createProgram() {
|
|
|
2708
3087
|
registerDiffCommand(program2);
|
|
2709
3088
|
registerCheckCommand(program2);
|
|
2710
3089
|
registerPushCommand(program2);
|
|
3090
|
+
registerTokenCommand(program2);
|
|
3091
|
+
registerWhoamiCommand(program2);
|
|
2711
3092
|
registerExplainCommand(program2);
|
|
2712
3093
|
registerIgnoresCommand(program2);
|
|
2713
3094
|
return program2;
|
package/package.json
CHANGED