skeptic-cli 0.2.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/package.json ADDED
@@ -0,0 +1,110 @@
1
+ {
2
+ "name": "skeptic-cli",
3
+ "version": "0.2.0",
4
+ "description": "CLI E2E testing for AI agents — TypeScript test runner with AI assertions, observability, and snapshot-based discovery",
5
+ "type": "module",
6
+ "bin": {
7
+ "skeptic": "bin/launcher.mjs"
8
+ },
9
+ "main": "./dist/index.mjs",
10
+ "module": "./dist/index.mjs",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/index.mjs",
15
+ "types": "./dist/index.d.ts"
16
+ },
17
+ "./test": {
18
+ "import": "./dist/index.mjs",
19
+ "types": "./dist/index.d.ts"
20
+ },
21
+ "./engine": {
22
+ "import": "./dist/index.mjs",
23
+ "types": "./dist/index.d.ts"
24
+ },
25
+ "./package.json": "./package.json"
26
+ },
27
+ "scripts": {
28
+ "build": "tsup",
29
+ "dev": "tsup --watch",
30
+ "test": "vitest run",
31
+ "test:watch": "vitest",
32
+ "check": "tsc --noEmit",
33
+ "clean": "rm -rf dist",
34
+ "postinstall": "node ./scripts/install-agent-skills.mjs"
35
+ },
36
+ "engines": {
37
+ "node": ">=22"
38
+ },
39
+ "files": [
40
+ "agent-skills",
41
+ "dist",
42
+ "bin/launcher.mjs",
43
+ "scripts/install-agent-skills.mjs",
44
+ "README.md",
45
+ "AGENTS.md",
46
+ "LICENSES.md"
47
+ ],
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/iamjr15/skeptic.git"
51
+ },
52
+ "homepage": "https://github.com/iamjr15/skeptic",
53
+ "author": "iamjr15",
54
+ "keywords": [
55
+ "testing",
56
+ "e2e",
57
+ "playwright",
58
+ "typescript",
59
+ "cli",
60
+ "ai",
61
+ "agents",
62
+ "browser"
63
+ ],
64
+ "license": "MIT",
65
+ "dependencies": {
66
+ "@playwright/test": "^1.59.1",
67
+ "cli-truncate": "^5.2.0",
68
+ "fast-glob": "^3.3.2",
69
+ "figures": "^6.1.0",
70
+ "ink": "^7.0.2",
71
+ "ink-spinner": "^5.0.0",
72
+ "oxc-resolver": "^11.19.1",
73
+ "playwright": "^1.52.0",
74
+ "playwright-core": "^1.52.0",
75
+ "pretty-ms": "^9.3.0",
76
+ "react": "^19.2.6",
77
+ "string-width": "^8.2.1",
78
+ "tsx": "^4.19.0",
79
+ "typescript": "^5.8.3"
80
+ },
81
+ "optionalDependencies": {
82
+ "accessibility-checker-engine": "^4.0.0",
83
+ "better-sqlite3": "^12.9.0"
84
+ },
85
+ "devDependencies": {
86
+ "@agentclientprotocol/sdk": "^0.20.0",
87
+ "@axe-core/playwright": "^4.11.2",
88
+ "@faker-js/faker": "^9.0.0",
89
+ "@google/generative-ai": "^0.24.1",
90
+ "@modelcontextprotocol/sdk": "^1.29.0",
91
+ "@types/better-sqlite3": "^7.6.13",
92
+ "@types/node": "^22.15.3",
93
+ "@types/pngjs": "^6.0.5",
94
+ "@types/react": "^19.2.14",
95
+ "chalk": "^5.4.1",
96
+ "chokidar": "^5.0.0",
97
+ "commander": "^13.1.0",
98
+ "fast-xml-parser": "^5.2.3",
99
+ "glob": "^11.0.2",
100
+ "ink-testing-library": "^4.0.0",
101
+ "minimatch": "^10.0.0",
102
+ "pixelmatch": "^7.1.0",
103
+ "pngjs": "^7.0.0",
104
+ "tsup": "^8.5.1",
105
+ "vitest": "^3.1.2",
106
+ "web-vitals": "^4.2.4",
107
+ "yaml": "^2.7.1",
108
+ "zod": "^3.24.4"
109
+ }
110
+ }
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const SKILL_NAME = "skeptic";
8
+ const MANAGED_MARKER = "skeptic-agent-skill: managed by skeptic-cli";
9
+ const RECOVERABLE_ENTRIES = new Set([".DS_Store", "Thumbs.db"]);
10
+ const SUPPORTED_AGENTS = ["claude", "codex", "cursor", "opencode"];
11
+
12
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
13
+ const packageRoot = path.resolve(scriptDir, "..");
14
+ const sourceDir = path.join(packageRoot, "agent-skills", SKILL_NAME);
15
+
16
+ const skipRequested =
17
+ process.env.SKEPTIC_SKIP_AGENT_SKILL_INSTALL === "1" ||
18
+ process.env.SKEPTIC_INSTALL_AGENT_SKILLS === "0";
19
+ const ciSkip =
20
+ process.env.CI && process.env.SKEPTIC_INSTALL_AGENT_SKILLS !== "1";
21
+
22
+ if (skipRequested || ciSkip) {
23
+ process.exit(0);
24
+ }
25
+
26
+ try {
27
+ if (!fs.existsSync(path.join(sourceDir, "SKILL.md"))) {
28
+ console.warn("skeptic-cli: bundled agent skill not found; skipping skill install");
29
+ process.exit(0);
30
+ }
31
+
32
+ const agents = parseAgents(process.env.SKEPTIC_AGENT_SKILLS);
33
+ const results = agents.map((agent) => installSkill(targetDirForAgent(agent)));
34
+ const changed = results.filter((result) => result === "installed" || result === "updated").length;
35
+
36
+ if (changed > 0 && process.env.npm_config_loglevel !== "silent") {
37
+ console.log(`skeptic-cli: installed agent skills for ${changed} coding agent location(s)`);
38
+ }
39
+ } catch (error) {
40
+ const message = error instanceof Error ? error.message : String(error);
41
+ console.warn(`skeptic-cli: agent skill install skipped: ${message}`);
42
+ }
43
+
44
+ function parseAgents(value) {
45
+ if (!value) return SUPPORTED_AGENTS;
46
+ const requested = value
47
+ .split(",")
48
+ .map((entry) => entry.trim().toLowerCase())
49
+ .filter(Boolean);
50
+ if (requested.includes("all")) return SUPPORTED_AGENTS;
51
+ return requested.filter((entry) => SUPPORTED_AGENTS.includes(entry));
52
+ }
53
+
54
+ function targetDirForAgent(agent) {
55
+ const home = process.env.HOME || os.homedir();
56
+ if (agent === "codex") {
57
+ return path.join(process.env.CODEX_HOME || path.join(home, ".codex"), "skills", SKILL_NAME);
58
+ }
59
+
60
+ const baseByAgent = {
61
+ claude: ".claude/skills",
62
+ cursor: ".cursor/skills",
63
+ opencode: ".opencode/skills",
64
+ };
65
+ return path.join(home, baseByAgent[agent], SKILL_NAME);
66
+ }
67
+
68
+ function installSkill(targetDir) {
69
+ const targetSkillPath = path.join(targetDir, "SKILL.md");
70
+
71
+ if (sameTree(sourceDir, targetDir)) return "already-installed";
72
+
73
+ const existing = lstat(targetDir);
74
+ if (existing) {
75
+ if (existing.isSymbolicLink()) return "skipped";
76
+
77
+ if (existing.isDirectory()) {
78
+ if (!fs.existsSync(targetSkillPath)) {
79
+ if (isRecoverableDirectory(targetDir)) {
80
+ fs.rmSync(targetDir, { recursive: true, force: true });
81
+ } else {
82
+ return "skipped";
83
+ }
84
+ } else if (!isManagedSkill(targetSkillPath)) {
85
+ return "skipped";
86
+ } else {
87
+ fs.rmSync(targetDir, { recursive: true, force: true });
88
+ }
89
+ } else if (existing.isFile()) {
90
+ if (!isManagedSkill(targetDir)) return "skipped";
91
+ fs.unlinkSync(targetDir);
92
+ } else {
93
+ return "skipped";
94
+ }
95
+ }
96
+
97
+ fs.mkdirSync(path.dirname(targetDir), { recursive: true });
98
+ fs.cpSync(sourceDir, targetDir, { recursive: true });
99
+ return existing ? "updated" : "installed";
100
+ }
101
+
102
+ function lstat(targetPath) {
103
+ try {
104
+ return fs.lstatSync(targetPath);
105
+ } catch (error) {
106
+ if (error instanceof Error && error.code === "ENOENT") return null;
107
+ throw error;
108
+ }
109
+ }
110
+
111
+ function isRecoverableDirectory(targetDir) {
112
+ return fs.readdirSync(targetDir).every((entry) => RECOVERABLE_ENTRIES.has(entry));
113
+ }
114
+
115
+ function isManagedSkill(skillPath) {
116
+ const content = fs.readFileSync(skillPath, "utf8");
117
+ return (
118
+ content.includes(MANAGED_MARKER) ||
119
+ content.includes("# skeptic E2E Testing") ||
120
+ content.includes("skeptic is a TypeScript test runner for AI agents")
121
+ );
122
+ }
123
+
124
+ function sameTree(sourcePath, targetPath) {
125
+ const sourceStats = lstat(sourcePath);
126
+ const targetStats = lstat(targetPath);
127
+ if (!sourceStats || !targetStats) return false;
128
+ if (sourceStats.isSymbolicLink() || targetStats.isSymbolicLink()) return false;
129
+
130
+ if (sourceStats.isDirectory() && targetStats.isDirectory()) {
131
+ const sourceEntries = fs.readdirSync(sourcePath).sort();
132
+ const targetEntries = fs.readdirSync(targetPath).sort();
133
+ if (sourceEntries.length !== targetEntries.length) return false;
134
+ return sourceEntries.every((entry, index) => {
135
+ if (entry !== targetEntries[index]) return false;
136
+ return sameTree(path.join(sourcePath, entry), path.join(targetPath, entry));
137
+ });
138
+ }
139
+
140
+ if (sourceStats.isFile() && targetStats.isFile()) {
141
+ return fs.readFileSync(sourcePath).equals(fs.readFileSync(targetPath));
142
+ }
143
+
144
+ return false;
145
+ }