atomism 0.1.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/LICENSE +21 -0
- package/README.md +210 -0
- package/dist/chunk-34O5KJWR.js +81 -0
- package/dist/chunk-34O5KJWR.js.map +1 -0
- package/dist/chunk-55AP34JO.js +116 -0
- package/dist/chunk-55AP34JO.js.map +1 -0
- package/dist/chunk-6MDHM2B4.js +17 -0
- package/dist/chunk-6MDHM2B4.js.map +1 -0
- package/dist/chunk-GU2R4KLP.js +43 -0
- package/dist/chunk-GU2R4KLP.js.map +1 -0
- package/dist/chunk-H7WC3NXZ.js +39 -0
- package/dist/chunk-H7WC3NXZ.js.map +1 -0
- package/dist/chunk-P33CQFMY.js +329 -0
- package/dist/chunk-P33CQFMY.js.map +1 -0
- package/dist/chunk-P6X7T4KA.js +200 -0
- package/dist/chunk-P6X7T4KA.js.map +1 -0
- package/dist/chunk-PLQJM2KT.js +9 -0
- package/dist/chunk-PLQJM2KT.js.map +1 -0
- package/dist/chunk-RS2IEGW3.js +10 -0
- package/dist/chunk-RS2IEGW3.js.map +1 -0
- package/dist/chunk-S6Z5G5DB.js +84 -0
- package/dist/chunk-S6Z5G5DB.js.map +1 -0
- package/dist/chunk-UVUDQ4XP.js +259 -0
- package/dist/chunk-UVUDQ4XP.js.map +1 -0
- package/dist/chunk-UWVZQSP4.js +597 -0
- package/dist/chunk-UWVZQSP4.js.map +1 -0
- package/dist/chunk-YKJO3ZFY.js +308 -0
- package/dist/chunk-YKJO3ZFY.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +152 -0
- package/dist/cli.js.map +1 -0
- package/dist/create-atom-AXPDBYQL.js +153 -0
- package/dist/create-atom-AXPDBYQL.js.map +1 -0
- package/dist/escalate-BTEJT5NL.js +211 -0
- package/dist/escalate-BTEJT5NL.js.map +1 -0
- package/dist/extract-RPKCTINT.js +514 -0
- package/dist/extract-RPKCTINT.js.map +1 -0
- package/dist/graduate-453M7ZRQ.js +222 -0
- package/dist/graduate-453M7ZRQ.js.map +1 -0
- package/dist/helpers-PJPFPYBQ.js +11 -0
- package/dist/helpers-PJPFPYBQ.js.map +1 -0
- package/dist/history-OPD7NLZW.js +258 -0
- package/dist/history-OPD7NLZW.js.map +1 -0
- package/dist/import-generator-4CKRBMTE.js +1864 -0
- package/dist/import-generator-4CKRBMTE.js.map +1 -0
- package/dist/index.d.ts +230 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/init-2FINDMYK.js +741 -0
- package/dist/init-2FINDMYK.js.map +1 -0
- package/dist/list-NEBVBGG3.js +71 -0
- package/dist/list-NEBVBGG3.js.map +1 -0
- package/dist/parser-3BILOSOO.js +157 -0
- package/dist/parser-3BILOSOO.js.map +1 -0
- package/dist/plan-DNVARHWH.js +249 -0
- package/dist/plan-DNVARHWH.js.map +1 -0
- package/dist/register-XTRMSH7Y.js +91 -0
- package/dist/register-XTRMSH7Y.js.map +1 -0
- package/dist/revert-J4CRDE2K.js +87 -0
- package/dist/revert-J4CRDE2K.js.map +1 -0
- package/dist/run-3GI3SBYL.js +188 -0
- package/dist/run-3GI3SBYL.js.map +1 -0
- package/dist/scan-generators-ST4TBEY7.js +375 -0
- package/dist/scan-generators-ST4TBEY7.js.map +1 -0
- package/dist/signatures-K5QIL4WG.js +258 -0
- package/dist/signatures-K5QIL4WG.js.map +1 -0
- package/dist/skills-assign-IHOXX4AI.js +182 -0
- package/dist/skills-assign-IHOXX4AI.js.map +1 -0
- package/dist/skills-load-JSD5UG2K.js +20 -0
- package/dist/skills-load-JSD5UG2K.js.map +1 -0
- package/dist/skills-scan-WACJFRJN.js +25 -0
- package/dist/skills-scan-WACJFRJN.js.map +1 -0
- package/dist/skills-suggest-JFI2NUJI.js +269 -0
- package/dist/skills-suggest-JFI2NUJI.js.map +1 -0
- package/dist/status-KQVSAZFR.js +111 -0
- package/dist/status-KQVSAZFR.js.map +1 -0
- package/dist/suggest-IFFJQFIW.js +183 -0
- package/dist/suggest-IFFJQFIW.js.map +1 -0
- package/dist/test-HP3FG3MO.js +152 -0
- package/dist/test-HP3FG3MO.js.map +1 -0
- package/dist/test-gen-2ZGPOP35.js +347 -0
- package/dist/test-gen-2ZGPOP35.js.map +1 -0
- package/dist/trust-4R26DULG.js +248 -0
- package/dist/trust-4R26DULG.js.map +1 -0
- package/dist/validate-generator-46H2LYYQ.js +410 -0
- package/dist/validate-generator-46H2LYYQ.js.map +1 -0
- package/dist/workflow-5UVLBS7J.js +655 -0
- package/dist/workflow-5UVLBS7J.js.map +1 -0
- package/package.json +84 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ATOMIC_DIR,
|
|
3
|
+
TRUST_FILE,
|
|
4
|
+
TrustFile,
|
|
5
|
+
createTrustFile,
|
|
6
|
+
fileExists,
|
|
7
|
+
getRegistryPath,
|
|
8
|
+
loadRegistry,
|
|
9
|
+
migrateTrustData
|
|
10
|
+
} from "./chunk-YKJO3ZFY.js";
|
|
11
|
+
import "./chunk-PLQJM2KT.js";
|
|
12
|
+
|
|
13
|
+
// src/commands/status.ts
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
import { readFile } from "fs/promises";
|
|
16
|
+
async function statusCommand(options) {
|
|
17
|
+
const projectRoot = process.cwd();
|
|
18
|
+
const registryPath = getRegistryPath(projectRoot);
|
|
19
|
+
const trustPath = join(projectRoot, ATOMIC_DIR, TRUST_FILE);
|
|
20
|
+
if (!await fileExists(registryPath)) {
|
|
21
|
+
const result2 = {
|
|
22
|
+
initialized: false,
|
|
23
|
+
atoms: { count: 0, list: [] },
|
|
24
|
+
stacks: { count: 0, atomBased: 0, generatorBased: 0 },
|
|
25
|
+
trust: { levels: { new: 0, proven: 0, trusted: 0 } },
|
|
26
|
+
recentRuns: []
|
|
27
|
+
};
|
|
28
|
+
if (options.json) {
|
|
29
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
30
|
+
} else {
|
|
31
|
+
console.log("atomic status");
|
|
32
|
+
console.log("");
|
|
33
|
+
console.log("\u26A0\uFE0F Not initialized. Run `atomic init` first.");
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
let registry;
|
|
38
|
+
try {
|
|
39
|
+
registry = await loadRegistry(registryPath);
|
|
40
|
+
} catch {
|
|
41
|
+
console.error("Failed to read registry. Is this a valid Atomic project?");
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
let trust = createTrustFile();
|
|
46
|
+
if (await fileExists(trustPath)) {
|
|
47
|
+
try {
|
|
48
|
+
const raw = JSON.parse(await readFile(trustPath, "utf-8"));
|
|
49
|
+
trust = TrustFile.parse(migrateTrustData(raw));
|
|
50
|
+
} catch {
|
|
51
|
+
console.error("Failed to read trust file.");
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const atoms = Object.values(registry.atoms ?? {});
|
|
57
|
+
const atomList = atoms.map((a) => ({ name: a.name, registeredAt: a.registeredAt })).sort((a, b) => b.registeredAt.localeCompare(a.registeredAt)).slice(0, 5);
|
|
58
|
+
const atomBased = atoms.filter((a) => !a.graduated).length;
|
|
59
|
+
const generatorBased = atoms.filter((a) => a.graduated).length;
|
|
60
|
+
const trustLevels = { new: 0, proven: 0, trusted: 0 };
|
|
61
|
+
for (const capability of Object.values(trust.stacks)) {
|
|
62
|
+
const level = capability.level;
|
|
63
|
+
if (level in trustLevels) {
|
|
64
|
+
trustLevels[level]++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const result = {
|
|
68
|
+
initialized: true,
|
|
69
|
+
atoms: {
|
|
70
|
+
count: atoms.length,
|
|
71
|
+
list: atomList
|
|
72
|
+
},
|
|
73
|
+
stacks: {
|
|
74
|
+
count: atoms.length,
|
|
75
|
+
atomBased,
|
|
76
|
+
generatorBased
|
|
77
|
+
},
|
|
78
|
+
trust: { levels: trustLevels },
|
|
79
|
+
recentRuns: []
|
|
80
|
+
// TODO: Implement when run history is available
|
|
81
|
+
};
|
|
82
|
+
if (options.json) {
|
|
83
|
+
console.log(JSON.stringify(result, null, 2));
|
|
84
|
+
} else {
|
|
85
|
+
console.log("atomic status");
|
|
86
|
+
console.log("");
|
|
87
|
+
console.log(`\u{1F4E6} Atoms: ${atoms.length}`);
|
|
88
|
+
if (atomList.length > 0) {
|
|
89
|
+
for (const atom of atomList) {
|
|
90
|
+
console.log(` \u2022 ${atom.name}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
console.log("");
|
|
94
|
+
console.log(`\u{1F527} Stacks: ${atoms.length}`);
|
|
95
|
+
if (atoms.length > 0) {
|
|
96
|
+
console.log(` \u2022 ${atomBased} atom-based`);
|
|
97
|
+
console.log(` \u2022 ${generatorBased} generator-based`);
|
|
98
|
+
}
|
|
99
|
+
console.log("");
|
|
100
|
+
console.log("\u{1F6E1}\uFE0F Trust levels:");
|
|
101
|
+
console.log(` \u2022 new: ${trustLevels.new}`);
|
|
102
|
+
console.log(` \u2022 proven: ${trustLevels.proven}`);
|
|
103
|
+
console.log(` \u2022 trusted: ${trustLevels.trusted}`);
|
|
104
|
+
console.log("");
|
|
105
|
+
console.log("\u{1F4CA} Recent runs: (run history not yet available)");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export {
|
|
109
|
+
statusCommand
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=status-KQVSAZFR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/status.ts"],"sourcesContent":["/**\n * Status command implementation for the atomic CLI.\n *\n * This module implements the `atomic status` command which displays\n * the current state of the atomic project including atoms, stacks,\n * and trust levels.\n *\n * @module status\n */\n\nimport { join } from 'node:path';\nimport { readFile } from 'node:fs/promises';\nimport { ATOMIC_DIR, TRUST_FILE, fileExists, loadRegistry, getRegistryPath } from '../storage/index.js';\nimport type { RegistryFile } from '../schemas/registry.js';\nimport { TrustFile, createTrustFile, migrateTrustData } from '../schemas/trust.js';\n\n/**\n * Options for the status command.\n */\nexport interface StatusOptions {\n json?: boolean;\n}\n\n\n/**\n * Result of the status command containing project state.\n */\nexport interface StatusResult {\n initialized: boolean;\n atoms: {\n count: number;\n list: Array<{ name: string; registeredAt: string }>;\n };\n stacks: {\n count: number;\n atomBased: number;\n generatorBased: number;\n };\n trust: {\n levels: { new: number; proven: number; trusted: number };\n };\n recentRuns: Array<{\n id: string;\n atomName: string;\n success: boolean;\n timestamp: string;\n }>;\n}\n\n/**\n * Display the current state of the atomic project.\n *\n * Shows initialization status, registered atoms, stacks (with\n * implementation type breakdown), trust levels, and recent runs.\n *\n * @param options - Command options including output format\n */\nexport async function statusCommand(options: StatusOptions): Promise<void> {\n const projectRoot = process.cwd();\n const registryPath = getRegistryPath(projectRoot);\n const trustPath = join(projectRoot, ATOMIC_DIR, TRUST_FILE);\n\n // Check if initialized\n if (!(await fileExists(registryPath))) {\n const result: StatusResult = {\n initialized: false,\n atoms: { count: 0, list: [] },\n stacks: { count: 0, atomBased: 0, generatorBased: 0 },\n trust: { levels: { new: 0, proven: 0, trusted: 0 } },\n recentRuns: [],\n };\n\n if (options.json) {\n console.log(JSON.stringify(result, null, 2));\n } else {\n console.log('atomic status');\n console.log('');\n console.log('⚠️ Not initialized. Run `atomic init` first.');\n }\n return;\n }\n\n // Load registry\n let registry: RegistryFile;\n try {\n registry = await loadRegistry(registryPath);\n } catch {\n console.error('Failed to read registry. Is this a valid Atomic project?');\n process.exitCode = 1;\n return;\n }\n\n // Load trust file\n let trust: TrustFile = createTrustFile();\n if (await fileExists(trustPath)) {\n try {\n const raw = JSON.parse(await readFile(trustPath, 'utf-8'));\n trust = TrustFile.parse(migrateTrustData(raw));\n } catch {\n console.error('Failed to read trust file.');\n process.exitCode = 1;\n return;\n }\n }\n\n // Count atoms\n const atoms = Object.values(registry.atoms ?? {});\n const atomList = atoms\n .map((a) => ({ name: a.name, registeredAt: a.registeredAt }))\n .sort((a, b) => b.registeredAt.localeCompare(a.registeredAt))\n .slice(0, 5);\n\n // Count graduated atoms as generator-based\n const atomBased = atoms.filter((a) => !a.graduated).length;\n const generatorBased = atoms.filter((a) => a.graduated).length;\n\n // Count trust levels\n const trustLevels = { new: 0, proven: 0, trusted: 0 };\n for (const capability of Object.values(trust.stacks)) {\n const level = capability.level as keyof typeof trustLevels;\n if (level in trustLevels) {\n trustLevels[level]++;\n }\n }\n\n // Build result\n const result: StatusResult = {\n initialized: true,\n atoms: {\n count: atoms.length,\n list: atomList,\n },\n stacks: {\n count: atoms.length,\n atomBased,\n generatorBased,\n },\n trust: { levels: trustLevels },\n recentRuns: [], // TODO: Implement when run history is available\n };\n\n if (options.json) {\n console.log(JSON.stringify(result, null, 2));\n } else {\n console.log('atomic status');\n console.log('');\n\n // Atoms section\n console.log(`📦 Atoms: ${atoms.length}`);\n if (atomList.length > 0) {\n for (const atom of atomList) {\n console.log(` • ${atom.name}`);\n }\n }\n\n // Stacks section\n console.log('');\n console.log(`🔧 Stacks: ${atoms.length}`);\n if (atoms.length > 0) {\n console.log(` • ${atomBased} atom-based`);\n console.log(` • ${generatorBased} generator-based`);\n }\n\n // Trust section\n console.log('');\n console.log('🛡️ Trust levels:');\n console.log(` • new: ${trustLevels.new}`);\n console.log(` • proven: ${trustLevels.proven}`);\n console.log(` • trusted: ${trustLevels.trusted}`);\n\n // Recent runs (placeholder)\n console.log('');\n console.log('📊 Recent runs: (run history not yet available)');\n }\n}\n"],"mappings":"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","names":["result"]}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import {
|
|
2
|
+
withErrorHandling
|
|
3
|
+
} from "./chunk-GU2R4KLP.js";
|
|
4
|
+
import "./chunk-YKJO3ZFY.js";
|
|
5
|
+
import {
|
|
6
|
+
fmt
|
|
7
|
+
} from "./chunk-S6Z5G5DB.js";
|
|
8
|
+
import "./chunk-PLQJM2KT.js";
|
|
9
|
+
|
|
10
|
+
// src/commands/suggest.ts
|
|
11
|
+
import { readdir, readFile } from "fs/promises";
|
|
12
|
+
import { join, relative } from "path";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
var SuggestOptionsSchema = z.object({
|
|
15
|
+
path: z.string().optional(),
|
|
16
|
+
limit: z.coerce.number().int().positive().default(20),
|
|
17
|
+
scanLimit: z.coerce.number().int().positive().default(1e3),
|
|
18
|
+
json: z.boolean().optional()
|
|
19
|
+
});
|
|
20
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
21
|
+
"node_modules",
|
|
22
|
+
"dist",
|
|
23
|
+
".atomic",
|
|
24
|
+
".git",
|
|
25
|
+
"coverage",
|
|
26
|
+
".next",
|
|
27
|
+
".turbo"
|
|
28
|
+
]);
|
|
29
|
+
var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".js", ".tsx", ".jsx"]);
|
|
30
|
+
function isTestFile(name) {
|
|
31
|
+
return /\.(test|spec)\.[jt]sx?$/.test(name) || /^test[-_]/.test(name);
|
|
32
|
+
}
|
|
33
|
+
function shouldSkipDir(name) {
|
|
34
|
+
return SKIP_DIRS.has(name) || name.startsWith("test-tmp-");
|
|
35
|
+
}
|
|
36
|
+
async function discoverFiles(dir, limit, files, skipped) {
|
|
37
|
+
if (files.length >= limit) return true;
|
|
38
|
+
let entries;
|
|
39
|
+
try {
|
|
40
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
41
|
+
} catch {
|
|
42
|
+
skipped.count++;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (files.length >= limit) return true;
|
|
47
|
+
const fullPath = join(dir, entry.name);
|
|
48
|
+
if (entry.isSymbolicLink()) {
|
|
49
|
+
skipped.count++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
if (shouldSkipDir(entry.name)) {
|
|
54
|
+
skipped.count++;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const limited = await discoverFiles(fullPath, limit, files, skipped);
|
|
58
|
+
if (limited) return true;
|
|
59
|
+
} else if (entry.isFile()) {
|
|
60
|
+
const ext = entry.name.slice(entry.name.lastIndexOf("."));
|
|
61
|
+
if (!SCAN_EXTENSIONS.has(ext)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (isTestFile(entry.name)) {
|
|
65
|
+
skipped.count++;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
files.push(fullPath);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
var SCORE_EXPORTED = 4;
|
|
74
|
+
var SCORE_TYPED_PARAMS = 2;
|
|
75
|
+
var SCORE_RETURN_TYPE = 2;
|
|
76
|
+
var SCORE_JSDOC = 1;
|
|
77
|
+
var SCORE_REASONABLE_SIZE = 1;
|
|
78
|
+
function scoreCandidate(c) {
|
|
79
|
+
let score = 0;
|
|
80
|
+
if (c.exported) score += SCORE_EXPORTED;
|
|
81
|
+
if (c.hasTypedParams) score += SCORE_TYPED_PARAMS;
|
|
82
|
+
if (c.hasReturnType) score += SCORE_RETURN_TYPE;
|
|
83
|
+
if (c.hasJSDoc) score += SCORE_JSDOC;
|
|
84
|
+
if (c.lineCount >= 10 && c.lineCount <= 100) score += SCORE_REASONABLE_SIZE;
|
|
85
|
+
return score;
|
|
86
|
+
}
|
|
87
|
+
async function suggestCommand(options) {
|
|
88
|
+
await withErrorHandling(options, async () => {
|
|
89
|
+
const parsed = SuggestOptionsSchema.parse(options);
|
|
90
|
+
const scanPath = parsed.path ?? process.cwd();
|
|
91
|
+
const limit = parsed.limit;
|
|
92
|
+
const scanLimit = parsed.scanLimit;
|
|
93
|
+
const files = [];
|
|
94
|
+
const skipped = { count: 0 };
|
|
95
|
+
const scanLimited = await discoverFiles(scanPath, scanLimit, files, skipped);
|
|
96
|
+
const { parseFile } = await import("./parser-3BILOSOO.js");
|
|
97
|
+
const { extractSignatures } = await import("./signatures-K5QIL4WG.js");
|
|
98
|
+
const candidates = [];
|
|
99
|
+
let errorCount = 0;
|
|
100
|
+
for (const filePath of files) {
|
|
101
|
+
let content;
|
|
102
|
+
try {
|
|
103
|
+
content = await readFile(filePath, "utf-8");
|
|
104
|
+
} catch {
|
|
105
|
+
errorCount++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const parseResult = await parseFile(content, filePath);
|
|
109
|
+
if (!parseResult.success) {
|
|
110
|
+
errorCount++;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (parseResult.language === "python") {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const sigs = extractSignatures(parseResult.ast, content, parseResult.comments);
|
|
117
|
+
for (const sig of sigs) {
|
|
118
|
+
if (!sig.exported) continue;
|
|
119
|
+
const candidate = {
|
|
120
|
+
name: sig.name,
|
|
121
|
+
filePath: relative(scanPath, filePath),
|
|
122
|
+
line: sig.line,
|
|
123
|
+
exported: sig.exported,
|
|
124
|
+
async: sig.async,
|
|
125
|
+
paramCount: sig.params.length,
|
|
126
|
+
hasTypedParams: sig.params.some((p) => p.type !== void 0),
|
|
127
|
+
hasReturnType: sig.returnType !== void 0,
|
|
128
|
+
hasJSDoc: sig.hasJSDoc,
|
|
129
|
+
lineCount: sig.endLine - sig.line + 1,
|
|
130
|
+
score: 0
|
|
131
|
+
};
|
|
132
|
+
candidate.score = scoreCandidate(candidate);
|
|
133
|
+
candidates.push(candidate);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
candidates.sort((a, b) => {
|
|
137
|
+
const diff = b.score - a.score;
|
|
138
|
+
if (diff !== 0) return diff;
|
|
139
|
+
return a.name.localeCompare(b.name);
|
|
140
|
+
});
|
|
141
|
+
const truncated = candidates.length > limit;
|
|
142
|
+
const results = candidates.slice(0, limit);
|
|
143
|
+
const result = {
|
|
144
|
+
candidates: results,
|
|
145
|
+
scanned: { files: files.length, skipped: skipped.count, errors: errorCount },
|
|
146
|
+
truncated,
|
|
147
|
+
scanLimited
|
|
148
|
+
};
|
|
149
|
+
if (options.json) {
|
|
150
|
+
console.log(JSON.stringify(result, null, 2));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (results.length === 0) {
|
|
154
|
+
console.log("No atom candidates found.");
|
|
155
|
+
console.log(fmt.dim(`Scanned ${files.length} files.`));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
console.log(fmt.bold(`Atom Candidates (${results.length} found, ${files.length} files scanned)`));
|
|
159
|
+
console.log("");
|
|
160
|
+
for (const c of results) {
|
|
161
|
+
const traits = [];
|
|
162
|
+
if (c.async) traits.push("async");
|
|
163
|
+
if (c.hasTypedParams) traits.push(`${c.paramCount} typed params`);
|
|
164
|
+
else if (c.paramCount > 0) traits.push(`${c.paramCount} params`);
|
|
165
|
+
if (c.hasReturnType) traits.push("has return type");
|
|
166
|
+
if (c.hasJSDoc) traits.push("has JSDoc");
|
|
167
|
+
console.log(` ${fmt.cyan(c.name.padEnd(25))} ${fmt.dim(c.filePath + ":" + c.line)}`);
|
|
168
|
+
console.log(` ${fmt.dim(traits.join(", "))}`);
|
|
169
|
+
console.log("");
|
|
170
|
+
}
|
|
171
|
+
if (truncated) {
|
|
172
|
+
console.log(fmt.dim(`Showing top ${limit} of ${candidates.length} candidates. Use --limit to see more.`));
|
|
173
|
+
}
|
|
174
|
+
if (scanLimited) {
|
|
175
|
+
console.log(fmt.dim(`Scan stopped after ${scanLimit} files. Use --scan-limit to increase.`));
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
export {
|
|
180
|
+
SuggestOptionsSchema,
|
|
181
|
+
suggestCommand
|
|
182
|
+
};
|
|
183
|
+
//# sourceMappingURL=suggest-IFFJQFIW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/suggest.ts"],"sourcesContent":["/**\n * Suggest command - Scan a codebase and recommend functions to atomize.\n *\n * Gap G3 of P4 audit.\n *\n * Discovers exported functions in JS/TS files using AST parsing,\n * ranks them by atomization fitness (typed params, return type, size),\n * and presents candidates to the user.\n */\n\nimport { readdir, readFile } from 'node:fs/promises';\nimport { join, relative } from 'node:path';\nimport { z } from 'zod';\nimport { fmt } from '../cli/format.js';\nimport { withErrorHandling } from './helpers.js';\n\n/**\n * Schema for suggest command options.\n * Coerces string CLI args to validated integers with safe defaults.\n */\nexport const SuggestOptionsSchema = z.object({\n path: z.string().optional(),\n limit: z.coerce.number().int().positive().default(20),\n scanLimit: z.coerce.number().int().positive().default(1000),\n json: z.boolean().optional(),\n});\n\n/**\n * Options for the suggest command (raw CLI input).\n */\nexport interface SuggestOptions {\n path?: string;\n limit?: string;\n scanLimit?: string;\n json?: boolean;\n}\n\n/**\n * A single suggestion candidate.\n */\ninterface Candidate {\n name: string;\n filePath: string;\n line: number;\n exported: boolean;\n async: boolean;\n paramCount: number;\n hasTypedParams: boolean;\n hasReturnType: boolean;\n hasJSDoc: boolean;\n lineCount: number;\n score: number;\n}\n\n/**\n * Result shape for JSON output.\n */\ninterface SuggestResult {\n candidates: Candidate[];\n scanned: { files: number; skipped: number; errors: number };\n truncated: boolean;\n scanLimited: boolean;\n}\n\n/** Directories to always skip during file discovery. */\nconst SKIP_DIRS = new Set([\n 'node_modules', 'dist', '.atomic', '.git', 'coverage', '.next', '.turbo',\n]);\n\n/** File extensions to scan. */\nconst SCAN_EXTENSIONS = new Set(['.ts', '.js', '.tsx', '.jsx']);\n\n/** Patterns indicating test files to skip. */\nfunction isTestFile(name: string): boolean {\n return /\\.(test|spec)\\.[jt]sx?$/.test(name) || /^test[-_]/.test(name);\n}\n\n/** Check if a directory name should be skipped. */\nfunction shouldSkipDir(name: string): boolean {\n return SKIP_DIRS.has(name) || name.startsWith('test-tmp-');\n}\n\n/**\n * Recursively discover source files, respecting scan limit.\n * Aborts traversal early when limit is reached.\n * Uses Dirent.isSymbolicLink() to skip symlinks.\n */\nasync function discoverFiles(\n dir: string,\n limit: number,\n files: string[],\n skipped: { count: number },\n): Promise<boolean> {\n if (files.length >= limit) return true; // scan-limited\n\n let entries: import('node:fs').Dirent[];\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n skipped.count++;\n return false;\n }\n\n for (const entry of entries) {\n if (files.length >= limit) return true;\n\n const fullPath = join(dir, entry.name);\n\n // Skip symlinks — Dirent provides this from readdir directly\n if (entry.isSymbolicLink()) {\n skipped.count++;\n continue;\n }\n\n if (entry.isDirectory()) {\n if (shouldSkipDir(entry.name)) {\n skipped.count++;\n continue;\n }\n const limited = await discoverFiles(fullPath, limit, files, skipped);\n if (limited) return true;\n } else if (entry.isFile()) {\n const ext = entry.name.slice(entry.name.lastIndexOf('.'));\n if (!SCAN_EXTENSIONS.has(ext)) {\n continue; // silently skip non-JS/TS files\n }\n if (isTestFile(entry.name)) {\n skipped.count++;\n continue;\n }\n files.push(fullPath);\n }\n }\n\n return false;\n}\n\n// Scoring weights for atomization candidates.\n// Exported functions are strongest signal (already have a public API surface).\n// Type annotations indicate well-defined contracts, important for schema generation.\n// JSDoc and reasonable size are minor quality signals.\nconst SCORE_EXPORTED = 4;\nconst SCORE_TYPED_PARAMS = 2;\nconst SCORE_RETURN_TYPE = 2;\nconst SCORE_JSDOC = 1;\nconst SCORE_REASONABLE_SIZE = 1;\n\n/**\n * Score a candidate for sorting. Higher = better atomization candidate.\n */\nfunction scoreCandidate(c: Candidate): number {\n let score = 0;\n if (c.exported) score += SCORE_EXPORTED;\n if (c.hasTypedParams) score += SCORE_TYPED_PARAMS;\n if (c.hasReturnType) score += SCORE_RETURN_TYPE;\n if (c.hasJSDoc) score += SCORE_JSDOC;\n if (c.lineCount >= 10 && c.lineCount <= 100) score += SCORE_REASONABLE_SIZE;\n return score;\n}\n\n/**\n * Suggest command handler.\n */\nexport async function suggestCommand(options: SuggestOptions): Promise<void> {\n await withErrorHandling(options, async () => {\n const parsed = SuggestOptionsSchema.parse(options);\n const scanPath = parsed.path ?? process.cwd();\n const limit = parsed.limit;\n const scanLimit = parsed.scanLimit;\n\n // 1. Discover files\n const files: string[] = [];\n const skipped = { count: 0 };\n const scanLimited = await discoverFiles(scanPath, scanLimit, files, skipped);\n\n // 2. Parse each file and extract signatures (parse-discard pattern)\n const { parseFile } = await import('../parser/index.js');\n const { extractSignatures } = await import('../parser/signatures.js');\n\n const candidates: Candidate[] = [];\n let errorCount = 0;\n\n for (const filePath of files) {\n let content: string;\n try {\n content = await readFile(filePath, 'utf-8');\n } catch {\n errorCount++;\n continue;\n }\n\n const parseResult = await parseFile(content, filePath);\n if (!parseResult.success) {\n errorCount++;\n continue;\n }\n\n // Only JS/TS family has typed Program AST; Python has unknown AST\n if (parseResult.language === 'python') {\n continue;\n }\n\n const sigs = extractSignatures(parseResult.ast, content, parseResult.comments);\n // AST is discarded after this point (goes out of scope)\n\n for (const sig of sigs) {\n if (!sig.exported) continue; // filter to exported only\n\n const candidate: Candidate = {\n name: sig.name,\n filePath: relative(scanPath, filePath),\n line: sig.line,\n exported: sig.exported,\n async: sig.async,\n paramCount: sig.params.length,\n hasTypedParams: sig.params.some(p => p.type !== undefined),\n hasReturnType: sig.returnType !== undefined,\n hasJSDoc: sig.hasJSDoc,\n lineCount: sig.endLine - sig.line + 1,\n score: 0,\n };\n candidate.score = scoreCandidate(candidate);\n candidates.push(candidate);\n }\n }\n\n // 3. Sort by score (descending), then name (ascending) for stability\n candidates.sort((a, b) => {\n const diff = b.score - a.score;\n if (diff !== 0) return diff;\n return a.name.localeCompare(b.name);\n });\n\n // 4. Truncate to limit\n const truncated = candidates.length > limit;\n const results = candidates.slice(0, limit);\n\n const result: SuggestResult = {\n candidates: results,\n scanned: { files: files.length, skipped: skipped.count, errors: errorCount },\n truncated,\n scanLimited,\n };\n\n // 5. Output\n if (options.json) {\n console.log(JSON.stringify(result, null, 2));\n return;\n }\n\n // Human-readable output\n if (results.length === 0) {\n console.log('No atom candidates found.');\n console.log(fmt.dim(`Scanned ${files.length} files.`));\n return;\n }\n\n console.log(fmt.bold(`Atom Candidates (${results.length} found, ${files.length} files scanned)`));\n console.log('');\n\n for (const c of results) {\n const traits: string[] = [];\n if (c.async) traits.push('async');\n if (c.hasTypedParams) traits.push(`${c.paramCount} typed params`);\n else if (c.paramCount > 0) traits.push(`${c.paramCount} params`);\n if (c.hasReturnType) traits.push('has return type');\n if (c.hasJSDoc) traits.push('has JSDoc');\n\n console.log(` ${fmt.cyan(c.name.padEnd(25))} ${fmt.dim(c.filePath + ':' + c.line)}`);\n console.log(` ${fmt.dim(traits.join(', '))}`);\n console.log('');\n }\n\n if (truncated) {\n console.log(fmt.dim(`Showing top ${limit} of ${candidates.length} candidates. Use --limit to see more.`));\n }\n if (scanLimited) {\n console.log(fmt.dim(`Scan stopped after ${scanLimit} files. Use --scan-limit to increase.`));\n }\n });\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveAtomPath
|
|
3
|
+
} from "./chunk-RS2IEGW3.js";
|
|
4
|
+
import {
|
|
5
|
+
fileExists,
|
|
6
|
+
getRegistryPath,
|
|
7
|
+
loadRegistry
|
|
8
|
+
} from "./chunk-YKJO3ZFY.js";
|
|
9
|
+
import {
|
|
10
|
+
toErrorMessage
|
|
11
|
+
} from "./chunk-PLQJM2KT.js";
|
|
12
|
+
|
|
13
|
+
// src/commands/test.ts
|
|
14
|
+
import { join, dirname } from "path";
|
|
15
|
+
import { spawn } from "child_process";
|
|
16
|
+
async function testCommand(atomName, options) {
|
|
17
|
+
const projectRoot = process.cwd();
|
|
18
|
+
const registryPath = getRegistryPath(projectRoot);
|
|
19
|
+
try {
|
|
20
|
+
const registry = await loadRegistry(registryPath);
|
|
21
|
+
const atom = registry.atoms[atomName];
|
|
22
|
+
if (!atom) {
|
|
23
|
+
const availableAtoms = Object.keys(registry.atoms);
|
|
24
|
+
if (availableAtoms.length === 0) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Atom '${atomName}' not found. No atoms are registered.
|
|
27
|
+
Register an atom with: atomic register <path>`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Atom '${atomName}' not found.
|
|
32
|
+
Available atoms: ${availableAtoms.join(", ")}`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
const atomDir = dirname(atom.path);
|
|
36
|
+
const authorTestPath = resolveAtomPath(atom.tests.path, atomDir);
|
|
37
|
+
const generatedTestPath = join(
|
|
38
|
+
projectRoot,
|
|
39
|
+
"tests",
|
|
40
|
+
"generated",
|
|
41
|
+
`${atomName}.generated.test.ts`
|
|
42
|
+
);
|
|
43
|
+
const testFiles = [];
|
|
44
|
+
if (await fileExists(authorTestPath)) {
|
|
45
|
+
testFiles.push(authorTestPath);
|
|
46
|
+
} else {
|
|
47
|
+
console.warn(`Warning: Author test file not found: ${authorTestPath}`);
|
|
48
|
+
}
|
|
49
|
+
if (await fileExists(generatedTestPath)) {
|
|
50
|
+
testFiles.push(generatedTestPath);
|
|
51
|
+
}
|
|
52
|
+
if (testFiles.length === 0) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`No test files found for atom '${atomName}'.
|
|
55
|
+
Expected author tests at: ${authorTestPath}
|
|
56
|
+
Generate tests with: atomic test-gen ${atom.path}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const result = await runVitest(testFiles, options, projectRoot);
|
|
60
|
+
if (options.json) {
|
|
61
|
+
const output = {
|
|
62
|
+
success: result.exitCode === 0,
|
|
63
|
+
atomName,
|
|
64
|
+
testFiles,
|
|
65
|
+
passed: result.passed,
|
|
66
|
+
failed: result.failed,
|
|
67
|
+
skipped: result.skipped,
|
|
68
|
+
duration: result.duration
|
|
69
|
+
};
|
|
70
|
+
if (options.coverage && result.coverageReport) {
|
|
71
|
+
output.coverageReport = result.coverageReport;
|
|
72
|
+
}
|
|
73
|
+
console.log(JSON.stringify(output, null, 2));
|
|
74
|
+
} else {
|
|
75
|
+
console.log(`
|
|
76
|
+
${result.passed} passed, ${result.failed} failed${result.skipped > 0 ? `, ${result.skipped} skipped` : ""}`);
|
|
77
|
+
console.log(`Duration: ${result.duration}ms`);
|
|
78
|
+
}
|
|
79
|
+
process.exit(result.exitCode);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (options.json) {
|
|
82
|
+
console.log(
|
|
83
|
+
JSON.stringify({
|
|
84
|
+
success: false,
|
|
85
|
+
error: toErrorMessage(error)
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
} else {
|
|
90
|
+
console.error(
|
|
91
|
+
`Error: ${toErrorMessage(error)}`
|
|
92
|
+
);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function runVitest(testFiles, options, projectRoot) {
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
const args = ["vitest", "run", "--reporter=json", ...testFiles];
|
|
100
|
+
if (options.coverage) {
|
|
101
|
+
args.push("--coverage");
|
|
102
|
+
}
|
|
103
|
+
const startTime = Date.now();
|
|
104
|
+
let stdout = "";
|
|
105
|
+
const child = spawn("npx", args, {
|
|
106
|
+
cwd: projectRoot,
|
|
107
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
108
|
+
});
|
|
109
|
+
child.stdout.on("data", (data) => {
|
|
110
|
+
stdout += data.toString();
|
|
111
|
+
});
|
|
112
|
+
child.stderr.on("data", (data) => {
|
|
113
|
+
if (!options.json) {
|
|
114
|
+
process.stderr.write(data);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
child.on("close", (code) => {
|
|
118
|
+
const duration = Date.now() - startTime;
|
|
119
|
+
let passed = 0;
|
|
120
|
+
let failed = 0;
|
|
121
|
+
let skipped = 0;
|
|
122
|
+
try {
|
|
123
|
+
const jsonMatch = stdout.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
|
|
124
|
+
if (jsonMatch) {
|
|
125
|
+
const json = JSON.parse(jsonMatch[0]);
|
|
126
|
+
passed = json.numPassedTests ?? 0;
|
|
127
|
+
failed = json.numFailedTests ?? 0;
|
|
128
|
+
skipped = json.numPendingTests ?? 0;
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
const passedMatch = stdout.match(/(\d+)\s+pass/i);
|
|
132
|
+
const failedMatch = stdout.match(/(\d+)\s+fail/i);
|
|
133
|
+
const skippedMatch = stdout.match(/(\d+)\s+skip/i);
|
|
134
|
+
if (passedMatch?.[1]) passed = parseInt(passedMatch[1], 10);
|
|
135
|
+
if (failedMatch?.[1]) failed = parseInt(failedMatch[1], 10);
|
|
136
|
+
if (skippedMatch?.[1]) skipped = parseInt(skippedMatch[1], 10);
|
|
137
|
+
}
|
|
138
|
+
resolve({
|
|
139
|
+
exitCode: code ?? 1,
|
|
140
|
+
passed,
|
|
141
|
+
failed,
|
|
142
|
+
skipped,
|
|
143
|
+
duration,
|
|
144
|
+
coverageReport: options.coverage ? "See coverage report in terminal output" : void 0
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
export {
|
|
150
|
+
testCommand
|
|
151
|
+
};
|
|
152
|
+
//# sourceMappingURL=test-HP3FG3MO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/test.ts"],"sourcesContent":["import { join, dirname } from 'node:path';\nimport { spawn } from 'node:child_process';\nimport { fileExists, loadRegistry, getRegistryPath } from '../storage/index.js';\nimport { toErrorMessage } from '../utils/errors.js';\nimport { resolveAtomPath } from '../utils/paths.js';\n\nexport interface TestOptions {\n json?: boolean;\n coverage?: boolean;\n}\n\nexport interface TestResult {\n success: boolean;\n atomName: string;\n testFiles: string[];\n passed: number;\n failed: number;\n skipped: number;\n duration: number;\n coverageReport?: string;\n}\n\n/**\n * Run the test suite for a registered atom.\n */\nexport async function testCommand(\n atomName: string,\n options: TestOptions\n): Promise<void> {\n const projectRoot = process.cwd();\n const registryPath = getRegistryPath(projectRoot);\n\n try {\n // Load registry\n const registry = await loadRegistry(registryPath);\n\n // Find the atom\n const atom = registry.atoms[atomName];\n if (!atom) {\n const availableAtoms = Object.keys(registry.atoms);\n if (availableAtoms.length === 0) {\n throw new Error(\n `Atom '${atomName}' not found. No atoms are registered.\\n` +\n `Register an atom with: atomic register <path>`\n );\n }\n throw new Error(\n `Atom '${atomName}' not found.\\n` +\n `Available atoms: ${availableAtoms.join(', ')}`\n );\n }\n\n // Resolve test file paths\n const atomDir = dirname(atom.path);\n const authorTestPath = resolveAtomPath(atom.tests.path, atomDir);\n\n const generatedTestPath = join(\n projectRoot,\n 'tests',\n 'generated',\n `${atomName}.generated.test.ts`\n );\n\n // Collect test files that exist\n const testFiles: string[] = [];\n\n if (await fileExists(authorTestPath)) {\n testFiles.push(authorTestPath);\n } else {\n console.warn(`Warning: Author test file not found: ${authorTestPath}`);\n }\n\n if (await fileExists(generatedTestPath)) {\n testFiles.push(generatedTestPath);\n }\n\n if (testFiles.length === 0) {\n throw new Error(\n `No test files found for atom '${atomName}'.\\n` +\n `Expected author tests at: ${authorTestPath}\\n` +\n `Generate tests with: atomic test-gen ${atom.path}`\n );\n }\n\n // Run Vitest\n const result = await runVitest(testFiles, options, projectRoot);\n\n if (options.json) {\n const output: TestResult = {\n success: result.exitCode === 0,\n atomName,\n testFiles,\n passed: result.passed,\n failed: result.failed,\n skipped: result.skipped,\n duration: result.duration,\n };\n if (options.coverage && result.coverageReport) {\n output.coverageReport = result.coverageReport;\n }\n console.log(JSON.stringify(output, null, 2));\n } else {\n console.log(`\\n${result.passed} passed, ${result.failed} failed${result.skipped > 0 ? `, ${result.skipped} skipped` : ''}`);\n console.log(`Duration: ${result.duration}ms`);\n }\n\n process.exit(result.exitCode);\n } catch (error) {\n if (options.json) {\n console.log(\n JSON.stringify({\n success: false,\n error: toErrorMessage(error),\n })\n );\n process.exit(1);\n } else {\n console.error(\n `Error: ${toErrorMessage(error)}`\n );\n process.exit(1);\n }\n }\n}\n\ninterface VitestResult {\n exitCode: number;\n passed: number;\n failed: number;\n skipped: number;\n duration: number;\n coverageReport?: string;\n}\n\nasync function runVitest(\n testFiles: string[],\n options: TestOptions,\n projectRoot: string\n): Promise<VitestResult> {\n return new Promise((resolve) => {\n const args = ['vitest', 'run', '--reporter=json', ...testFiles];\n\n if (options.coverage) {\n args.push('--coverage');\n }\n\n const startTime = Date.now();\n let stdout = '';\n\n const child = spawn('npx', args, {\n cwd: projectRoot,\n stdio: ['inherit', 'pipe', 'pipe'],\n });\n\n child.stdout.on('data', (data) => {\n stdout += data.toString();\n });\n\n child.stderr.on('data', (data) => {\n // Vitest outputs progress to stderr, show it in non-json mode\n if (!options.json) {\n process.stderr.write(data);\n }\n });\n\n child.on('close', (code) => {\n const duration = Date.now() - startTime;\n\n // Parse JSON reporter output\n let passed = 0;\n let failed = 0;\n let skipped = 0;\n\n try {\n // Vitest JSON output may have multiple JSON objects, find the test results\n const jsonMatch = stdout.match(/\\{[\\s\\S]*\"testResults\"[\\s\\S]*\\}/);\n if (jsonMatch) {\n const json = JSON.parse(jsonMatch[0]) as {\n testResults?: Array<{\n assertionResults?: Array<{ status: string }>;\n }>;\n numPassedTests?: number;\n numFailedTests?: number;\n numPendingTests?: number;\n };\n\n passed = json.numPassedTests ?? 0;\n failed = json.numFailedTests ?? 0;\n skipped = json.numPendingTests ?? 0;\n }\n } catch {\n // If JSON parsing fails, try to extract from output\n const passedMatch = stdout.match(/(\\d+)\\s+pass/i);\n const failedMatch = stdout.match(/(\\d+)\\s+fail/i);\n const skippedMatch = stdout.match(/(\\d+)\\s+skip/i);\n\n if (passedMatch?.[1]) passed = parseInt(passedMatch[1], 10);\n if (failedMatch?.[1]) failed = parseInt(failedMatch[1], 10);\n if (skippedMatch?.[1]) skipped = parseInt(skippedMatch[1], 10);\n }\n\n resolve({\n exitCode: code ?? 1,\n passed,\n failed,\n skipped,\n duration,\n coverageReport: options.coverage ? 'See coverage report in terminal output' : undefined,\n });\n });\n });\n}\n"],"mappings":"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","names":[]}
|