Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 5 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,21 @@
# effect-ts-skills

Reusable Effect-TS skills and compliance tooling for [Codex](https://github.com/openai/codex).
Codex plugin with an Effect-TS guide skill and a bundled `effect-ts-check` CLI.

## Plugin

This repository publishes one Codex plugin:

| Plugin | Path | Bundled skill |
|--------|------|---------------|
| `effect-ts-skills` | `plugins/effect-ts-skills` | `effect-ts-guide` |

The `effect-ts-guide` skill is intentionally kept inside the plugin. There is no separate root-level standalone skill copy.

## Installation
## Install

```bash
codex plugin marketplace add ProverCoderAI/effect-ts-skills
codex plugin add effect-ts-skills@effect-ts-skills
```

For local development from a checkout:

```bash
codex plugin marketplace add .
codex plugin add effect-ts-skills@effect-ts-skills
```

The repo marketplace entry points at `plugins/effect-ts-skills`, which is the canonical plugin directory.

After installation, start a new Codex thread and invoke the skill explicitly with `$effect-ts-guide` or ask for an Effect-TS implementation/review task.
## Use

## Development
Start a new Codex thread and ask for `$effect-ts-guide`, or ask Codex to review/fix Effect-TS code.

This repository is a [pnpm workspace](https://pnpm.io/workspaces).
## Develop

```bash
corepack pnpm install
corepack pnpm run sync:distribution
corepack pnpm run check
```

### Structure

```
plugins/effect-ts-skills/ # Installable Codex plugin with bundled effect-ts-guide skill
packages/effect-ts-check/ # Reusable Effect-TS compliance CLI
tools/ # Repo-level validation scripts
```

## License

ISC
4 changes: 4 additions & 0 deletions packages/effect-ts-check/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"./strict": {
"import": "./src/strict.mjs"
},
"./strict-format": {
"import": "./src/strict-format.mjs"
},
"./rules": {
"import": "./src/rules/index.mjs"
},
Expand All @@ -42,6 +45,7 @@
},
"dependencies": {
"@effect/eslint-plugin": "^0.3.2",
"@eslint-community/eslint-plugin-eslint-comments": "^4.7.2",
"@eslint/js": "^10.0.0",
"eslint": "^10.0.0",
"typescript-eslint": "^8.57.1"
Expand Down
6 changes: 6 additions & 0 deletions packages/effect-ts-check/src/base.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import tseslint from "typescript-eslint";
import { effectSyntaxRestrictions } from "./rules/index.mjs";

export const effectFileGlobs = Object.freeze(["**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}"]);
export const effectCoreFileGlobs = Object.freeze([
"**/src/core/**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}",
]);
export const effectCoreAxiomsFileGlobs = Object.freeze([
"**/src/core/axioms.{ts,tsx,mts,cts}",
]);
export const effectIgnoreGlobs = Object.freeze([
"tests/**",
"**/tests/**",
Expand Down
5 changes: 5 additions & 0 deletions packages/effect-ts-check/src/index.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
export { minimal } from "./minimal.mjs";
export {
effectCoreRestrictedImportPatterns,
effectRestrictedImportPatterns,
effectRestrictedImports,
effectCoreSyntaxRestrictions,
effectErrorBoundarySyntaxRestrictions,
effectHostSyntaxRestrictions,
effectStrictSyntaxRestrictions,
effectSyntaxRestrictions,
effectTypeRules,
} from "./rules/index.mjs";
export { getProfileConfig, lintPaths, main, parseArguments } from "./run.mjs";
export { strict } from "./strict.mjs";
export { strictFormat } from "./strict-format.mjs";
14 changes: 14 additions & 0 deletions packages/effect-ts-check/src/rules/imports.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,17 @@ export const effectRestrictedImportPatterns = Object.freeze([
message: "Do not import from node:* directly. Use @effect/platform services.",
},
]);

export const effectCoreRestrictedImportPatterns = Object.freeze([
{
group: [
"../shell/**",
"../../shell/**",
"../../../shell/**",
"./shell/**",
"src/shell/**",
"shell/**",
],
message: "CORE must not import from SHELL.",
},
]);
14 changes: 12 additions & 2 deletions packages/effect-ts-check/src/rules/index.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
export { effectRestrictedImportPatterns, effectRestrictedImports } from "./imports.mjs";
export { effectStrictSyntaxRestrictions, effectSyntaxRestrictions } from "./syntax.mjs";
export {
effectCoreRestrictedImportPatterns,
effectRestrictedImportPatterns,
effectRestrictedImports,
} from "./imports.mjs";
export {
effectCoreSyntaxRestrictions,
effectErrorBoundarySyntaxRestrictions,
effectHostSyntaxRestrictions,
effectStrictSyntaxRestrictions,
effectSyntaxRestrictions,
} from "./syntax.mjs";
export { effectTypeRules } from "./types.mjs";
24 changes: 23 additions & 1 deletion packages/effect-ts-check/src/rules/syntax.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const effectSyntaxRestrictions = Object.freeze([
},
]);

export const effectStrictSyntaxRestrictions = Object.freeze([
export const effectHostSyntaxRestrictions = Object.freeze([
{
selector: "CallExpression[callee.name='fetch']",
message: "Use @effect/platform HttpClient instead of fetch.",
Expand All @@ -50,6 +50,9 @@ export const effectStrictSyntaxRestrictions = Object.freeze([
selector: "CallExpression[callee.object.name='global'][callee.property.name='fetch']",
message: "Use @effect/platform HttpClient instead of global.fetch.",
},
]);

export const effectErrorBoundarySyntaxRestrictions = Object.freeze([
{
selector: "CallExpression[callee.property.name='catchAll']",
message: "Avoid catchAll that swallows typed errors; map or rethrow explicitly.",
Expand All @@ -62,8 +65,27 @@ export const effectStrictSyntaxRestrictions = Object.freeze([
selector: "TSTypeAssertion",
message: "Avoid casts in product code; keep them in one axioms boundary if needed.",
},
]);

export const effectCoreSyntaxRestrictions = Object.freeze([
{
selector: "TSUnknownKeyword",
message: "Use unknown only at shell boundaries with decoding.",
},
{
selector: "CallExpression[callee.property.name='runSyncExit']",
message: "Use Effect.runSyncExit only at shell/runtime boundaries.",
},
{
selector: "CallExpression[callee.property.name='runSync']",
message: "Use Effect.runSync only at shell/runtime boundaries.",
},
{
selector: "CallExpression[callee.property.name='runPromise']",
message: "Use Effect.runPromise only at shell/runtime boundaries.",
},
]);

export const effectStrictSyntaxRestrictions = Object.freeze([
...effectHostSyntaxRestrictions,
]);
17 changes: 16 additions & 1 deletion packages/effect-ts-check/src/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ESLint } from "eslint";

import { minimal } from "./minimal.mjs";
import { strict } from "./strict.mjs";
import { strictFormat } from "./strict-format.mjs";

export function parseArguments(argv) {
const result = {
Expand Down Expand Up @@ -40,6 +41,10 @@ export function parseArguments(argv) {
}

export function getProfileConfig(profile) {
if (profile === "strict-format") {
return strictFormat;
}

if (profile === "strict") {
return strict;
}
Expand All @@ -52,6 +57,13 @@ export function getProfileConfig(profile) {
}

function resolveProfileConfig(profile) {
if (profile === "strict-format") {
return {
ok: true,
config: strictFormat,
};
}

if (profile === "strict") {
return {
ok: true,
Expand All @@ -78,10 +90,13 @@ export function printUsage() {
"Usage:",
" effect-ts-check [paths...]",
" effect-ts-check --profile strict [paths...]",
" effect-ts-check --profile strict-format [paths...]",
"",
"Profiles:",
" minimal Default fast effect compliance check.",
" strict Adds import/type/host API policy checks.",
" strict Adds import/type/host API and Effect boundary policy checks.",
" strict-format",
" Runs strict plus the official @effect/dprint formatting preset.",
"",
].join("\n"),
);
Expand Down
10 changes: 10 additions & 0 deletions packages/effect-ts-check/src/strict-format.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as effectEslint from "@effect/eslint-plugin";

import { strict } from "./strict.mjs";

export const strictFormat = [
...strict,
...effectEslint.configs.dprint,
];

export default strictFormat;
73 changes: 64 additions & 9 deletions packages/effect-ts-check/src/strict.mjs
Original file line number Diff line number Diff line change
@@ -1,18 +1,50 @@
import * as effectEslint from "@effect/eslint-plugin";
import eslintComments from "@eslint-community/eslint-plugin-eslint-comments";
import tseslint from "typescript-eslint";

import { effectBaseConfig, effectFileGlobs } from "./base.mjs";
import {
effectCoreAxiomsFileGlobs,
effectCoreFileGlobs,
effectFileGlobs,
effectIgnoreConfig,
} from "./base.mjs";
import {
effectCoreRestrictedImportPatterns,
effectCoreSyntaxRestrictions,
effectErrorBoundarySyntaxRestrictions,
effectRestrictedImportPatterns,
effectRestrictedImports,
effectStrictSyntaxRestrictions,
effectSyntaxRestrictions,
effectTypeRules,
} from "./rules/index.mjs";

const strictSyntaxRestrictions = Object.freeze([
...effectSyntaxRestrictions,
...effectStrictSyntaxRestrictions,
]);

const coreSyntaxRestrictions = Object.freeze([
...strictSyntaxRestrictions,
...effectErrorBoundarySyntaxRestrictions,
...effectCoreSyntaxRestrictions,
]);

const coreAxiomsSyntaxRestrictions = Object.freeze([
...strictSyntaxRestrictions,
...effectErrorBoundarySyntaxRestrictions.filter((rule) =>
rule.selector !== "TSAsExpression" && rule.selector !== "TSTypeAssertion"
),
...effectCoreSyntaxRestrictions,
]);

const strictImportPatterns = Object.freeze([...effectRestrictedImportPatterns]);
const coreImportPatterns = Object.freeze([
...effectRestrictedImportPatterns,
...effectCoreRestrictedImportPatterns,
]);

export const strict = [
...effectBaseConfig,
...effectEslint.configs.dprint,
effectIgnoreConfig,
{
name: "effect-ts-check/strict",
files: effectFileGlobs,
Expand All @@ -23,22 +55,45 @@ export const strict = [
},
plugins: {
"@typescript-eslint": tseslint.plugin,
"eslint-comments": eslintComments,
},
rules: {
"no-console": "error",
"no-throw-literal": "error",
"no-restricted-imports": [
"error",
{
paths: effectRestrictedImports,
patterns: effectRestrictedImportPatterns,
patterns: strictImportPatterns,
},
],
"no-restricted-syntax": [
"no-restricted-syntax": ["error", ...strictSyntaxRestrictions],
"eslint-comments/no-use": "error",
"eslint-comments/no-unlimited-disable": "error",
"eslint-comments/disable-enable-pair": "error",
"eslint-comments/no-unused-disable": "error",
...effectTypeRules,
},
},
{
name: "effect-ts-check/strict-core",
files: effectCoreFileGlobs,
rules: {
"no-restricted-imports": [
"error",
...effectSyntaxRestrictions,
...effectStrictSyntaxRestrictions,
{
paths: effectRestrictedImports,
patterns: coreImportPatterns,
},
],
...effectTypeRules,
"no-restricted-syntax": ["error", ...coreSyntaxRestrictions],
},
},
{
name: "effect-ts-check/strict-core-axioms",
files: effectCoreAxiomsFileGlobs,
rules: {
"no-restricted-syntax": ["error", ...coreAxiomsSyntaxRestrictions],
},
},
];
Expand Down
17 changes: 17 additions & 0 deletions packages/effect-ts-check/tests/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,23 @@ test("cli supports strict profile", () => {
assert.match(result.stdout, /no-restricted-imports|@typescript-eslint\/no-explicit-any|no-console/)
})

test("cli supports strict-format profile", () => {
const fixture = writeTempFixture("pass.ts", "const value = 1;\nexport { value };\n")
const result = spawnSync(
process.execPath,
[cliPath, "--profile", "strict-format", fixture.path],
{
cwd: packageDir,
encoding: "utf8"
}
)
fixture.cleanup()

assert.equal(result.status, 0)
assert.equal(result.stdout, "")
assert.equal(result.stderr, "")
})

test("cli strict profile keeps minimal syntax checks", () => {
const fixture = writeTempFixture(
"fail.ts",
Expand Down
Loading