@aas-dev/stack 0.0.0-stage → 0.1.1
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 +97 -2
- package/bin/aas.js +2 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +15 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +75 -0
- package/dist/cursor-hook.d.ts +1 -0
- package/dist/cursor-hook.js +28 -0
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +162 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +5 -0
- package/dist/explain.d.ts +4 -0
- package/dist/explain.js +48 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/package-script.d.ts +3 -0
- package/dist/package-script.js +42 -0
- package/dist/plugin/aas.js +379 -0
- package/dist/policy.d.ts +1 -0
- package/dist/policy.js +124 -0
- package/dist/program.d.ts +3 -0
- package/dist/program.js +49 -0
- package/dist/run.d.ts +18 -0
- package/dist/run.js +99 -0
- package/dist/scaffold.d.ts +5 -0
- package/dist/scaffold.js +76 -0
- package/dist/templates.d.ts +6 -0
- package/dist/templates.js +37 -0
- package/dist/verify.d.ts +4 -0
- package/dist/verify.js +190 -0
- package/dist/workspace.d.ts +4 -0
- package/dist/workspace.js +76 -0
- package/package.json +54 -3
- package/plugin/aas.d.mts +8 -0
- package/plugin/aas.mjs +572 -0
- package/templates/AGENTS.md +147 -0
- package/templates/hooks.json +11 -0
- package/templates/pnpm-workspace.yaml +23 -0
- package/templates/pre-commit +3 -0
- package/templates/tsconfig.json +8 -0
- package/templates/verify.yml +20 -0
- package/templates/vite.config.ts +3 -0
- package/tsconfig.base.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kristoffer Aas
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,98 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@aas-dev/stack`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The `aas` command. One toolchain: Vite+ (`vp`) for lint, format, test, and library build, and `@effect/tsgo` for the typecheck. The package version in this tree is `0.1.1`.
|
|
4
|
+
|
|
5
|
+
Node `>=22.18` and pnpm `11.3.0`.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Put `pnpm-workspace.yaml` on disk before the first `pnpm install`. pnpm reads the overrides and `allowBuilds` while it builds the graph. `esbuild: true` lets the Vite+ install script run. Without it, pnpm `11.3.0` stops with `ERR_PNPM_IGNORED_BUILDS`. Extra `allowBuilds` keys are yours; `aas verify` does not call that drift.
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
packages:
|
|
13
|
+
- "packages/*"
|
|
14
|
+
|
|
15
|
+
verifyDepsBeforeRun: error
|
|
16
|
+
|
|
17
|
+
# Exact versions are the pin. The release-age gate would refuse the Effect 4
|
|
18
|
+
# and oxlint versions this stack just chose, so it stays off.
|
|
19
|
+
minimumReleaseAge: 0
|
|
20
|
+
|
|
21
|
+
overrides:
|
|
22
|
+
"vite@*": "npm:@voidzero-dev/vite-plus-core@1.4.0"
|
|
23
|
+
"vitest@*": "5.0.5"
|
|
24
|
+
|
|
25
|
+
peerDependencyRules:
|
|
26
|
+
allowAny:
|
|
27
|
+
- vite
|
|
28
|
+
- vitest
|
|
29
|
+
allowedVersions:
|
|
30
|
+
vite: "*"
|
|
31
|
+
vitest: "*"
|
|
32
|
+
|
|
33
|
+
allowBuilds:
|
|
34
|
+
esbuild: true
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The `vite` override aliases Vite to `@voidzero-dev/vite-plus-core`. That package's own version is `1.4.0`, which is outside Vitest's `vite@^6 || ^7 || ^8` peer range. `peerDependencyRules` is what makes pnpm treat the aliased package as satisfying that peer. `allowAny` and `allowedVersions: "*"` are the pair Vite+ writes for this override.
|
|
38
|
+
|
|
39
|
+
`minimumReleaseAge: 0` lets pnpm fetch the Effect and Vite+ versions this stack pins. A project with no `packages/` directory still installs: the glob matches nothing and the root package is the project.
|
|
40
|
+
|
|
41
|
+
Depend on `effect`, `@effect/vitest`, and `@types/node` as well. The project imports them.
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
pnpm add -D @aas-dev/stack@0.1.1 effect@4.0.6 @effect/vitest@4.0.6 @types/node@22.20.5
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
From a packed tarball the file name is `aas-dev-stack-0.1.1.tgz`:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"scripts": {
|
|
52
|
+
"verify": "aas verify"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@aas-dev/stack": "file:./aas-dev-stack-0.1.1.tgz",
|
|
56
|
+
"@effect/vitest": "4.0.6",
|
|
57
|
+
"@types/node": "22.20.5",
|
|
58
|
+
"effect": "4.0.6"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Init, the second install, verify
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
pnpm exec aas init
|
|
67
|
+
pnpm exec aas verify
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`aas init` writes the managed files and adds `"verify": "aas verify"` when that script is missing. When `pnpm-lock.yaml` already exists, init deletes `node_modules/.pnpm-workspace-state-v1.json` and runs `pnpm install`, so the lockfile overrides match the template. A second `pnpm install` that only prints "Already up to date" leaves those overrides stale, and the next `pnpm exec` fails with `ERR_PNPM_VERIFY_DEPS_BEFORE_RUN`.
|
|
71
|
+
|
|
72
|
+
If `pnpm exec` is already failing with that error, run `pnpm install`. When the workspace file changed and install does not refresh, run `node node_modules/@aas-dev/stack/bin/aas.js init`.
|
|
73
|
+
|
|
74
|
+
When `core.hooksPath` is already set, init prints the path and skips `vp hooks enable`. It does not take that path.
|
|
75
|
+
|
|
76
|
+
`aas verify` prints nothing when it passes. A failure is `file:line: reason`.
|
|
77
|
+
|
|
78
|
+
`aas explain` lists every ban and the approved alternative. `aas explain no-raw-io` is one rule. `aas doctor` checks the Node and pnpm pins, the hooks path, lockfile overrides, the patched `tsc`, and drift. It exits non-zero when a check fails.
|
|
79
|
+
|
|
80
|
+
## Managed files
|
|
81
|
+
|
|
82
|
+
| File | Role |
|
|
83
|
+
| ------------------------------ | --------------------------------------------------------------------- |
|
|
84
|
+
| `.cursor/hooks.json` | Cursor `beforeShellExecution` hook, `failClosed` |
|
|
85
|
+
| `.vite-hooks/pre-commit` | Runs `pnpm exec aas verify` |
|
|
86
|
+
| `pnpm-workspace.yaml` | Pins, overrides, peer rules, `allowBuilds`. Extra build keys are kept |
|
|
87
|
+
| `AGENTS.md` | The patterns the rules enforce |
|
|
88
|
+
| `.github/workflows/verify.yml` | CI runs `pnpm exec aas verify` |
|
|
89
|
+
| `vite.config.ts` | `defineConfig()` from `@aas-dev/stack/config` |
|
|
90
|
+
| `tsconfig.json` | Extends `@aas-dev/stack/tsconfig.base.json` |
|
|
91
|
+
|
|
92
|
+
`tsconfig.base.json` sets `noEmit`. `aas verify` runs `effect-tsgo patch` and then `tsc --noEmit`. That is the typecheck. Library output is `vp pack`, not `tsc`.
|
|
93
|
+
|
|
94
|
+
`aas sync` rewrites the managed files and keeps `allowBuilds` keys that are not in the template.
|
|
95
|
+
|
|
96
|
+
## Patterns the rules keep
|
|
97
|
+
|
|
98
|
+
Errors are a `Schema.TaggedError`. Config is `Config` from `.env.schema`, or `Config.orElse(() => Config.succeed(...))` for a computed fallback such as a homedir path. `process.env` stays banned. Services extend `Context.Service`. IO goes through Effect, except `node:net` and `node:tls`, which are allowed only in a file named `*.adapter.ts`. Tests use `it.effect` from `@effect/vitest`. The full text is the generated `AGENTS.md`. `aas explain` prints the same alternatives.
|
package/bin/aas.js
ADDED
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { main } from "./program.js";
|
|
2
|
+
import { NodeRuntime, NodeServices } from "@effect/platform-node";
|
|
3
|
+
import { Effect, Exit } from "effect";
|
|
4
|
+
// src/bin.ts
|
|
5
|
+
const teardown = (exit, onExit) => {
|
|
6
|
+
onExit(Exit.match(exit, {
|
|
7
|
+
onFailure: () => 1,
|
|
8
|
+
onSuccess: (code) => typeof code === "number" ? code : 1
|
|
9
|
+
}));
|
|
10
|
+
};
|
|
11
|
+
NodeRuntime.runMain(main(process.cwd()).pipe(Effect.provide(NodeServices.layer)), {
|
|
12
|
+
disableErrorReporting: true,
|
|
13
|
+
teardown
|
|
14
|
+
});
|
|
15
|
+
export {};
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { defineConfig as defineConfig$1 } from "vite-plus";
|
|
2
|
+
// src/config.ts
|
|
3
|
+
const pluginPath = `${import.meta.dirname}/../plugin/aas.mjs`;
|
|
4
|
+
const errors = (rules) => rules;
|
|
5
|
+
const rules = errors({
|
|
6
|
+
"aas/no-throw": "error",
|
|
7
|
+
"aas/no-try": "error",
|
|
8
|
+
"aas/no-error-class": "error",
|
|
9
|
+
"aas/no-process-env": "error",
|
|
10
|
+
"aas/no-type-assertion": "error",
|
|
11
|
+
"aas/no-explicit-any": "error",
|
|
12
|
+
"aas/no-non-null-assertion": "error",
|
|
13
|
+
"aas/no-bypass-comment": "error",
|
|
14
|
+
"aas/no-comments": "error",
|
|
15
|
+
"aas/effect-tests": "error",
|
|
16
|
+
"aas/no-raw-io": "error",
|
|
17
|
+
"aas/no-effect-run": "error",
|
|
18
|
+
"aas/no-module-let": "error",
|
|
19
|
+
"aas/no-plain-class": "error"
|
|
20
|
+
});
|
|
21
|
+
const defaultIgnore = [
|
|
22
|
+
"dist",
|
|
23
|
+
"vendor",
|
|
24
|
+
"node_modules",
|
|
25
|
+
"coverage",
|
|
26
|
+
".vite-hooks/_",
|
|
27
|
+
"packages/fence/fixtures"
|
|
28
|
+
];
|
|
29
|
+
const defineConfig = (options) => defineConfig$1({
|
|
30
|
+
lint: {
|
|
31
|
+
jsPlugins: [pluginPath],
|
|
32
|
+
options: {
|
|
33
|
+
denyWarnings: true,
|
|
34
|
+
respectEslintDisableDirectives: false
|
|
35
|
+
},
|
|
36
|
+
ignorePatterns: [...options?.ignorePatterns ?? defaultIgnore],
|
|
37
|
+
rules
|
|
38
|
+
},
|
|
39
|
+
fmt: {
|
|
40
|
+
printWidth: 100,
|
|
41
|
+
semi: false,
|
|
42
|
+
ignorePatterns: [
|
|
43
|
+
"node_modules",
|
|
44
|
+
"dist",
|
|
45
|
+
"vendor",
|
|
46
|
+
"coverage",
|
|
47
|
+
".vitest",
|
|
48
|
+
".vite-hooks/_"
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
test: {
|
|
52
|
+
include: ["**/*.test.ts"],
|
|
53
|
+
exclude: [
|
|
54
|
+
"**/node_modules/**",
|
|
55
|
+
"**/dist/**",
|
|
56
|
+
"**/fixtures/**",
|
|
57
|
+
"**/vendor/**"
|
|
58
|
+
],
|
|
59
|
+
passWithNoTests: false,
|
|
60
|
+
server: { deps: { inline: ["@effect/vitest"] } }
|
|
61
|
+
},
|
|
62
|
+
pack: {
|
|
63
|
+
entry: ["src/**/*.ts", "!src/**/*.test.ts"],
|
|
64
|
+
dts: true,
|
|
65
|
+
unbundle: true,
|
|
66
|
+
fixedExtension: false,
|
|
67
|
+
hash: false,
|
|
68
|
+
clean: true,
|
|
69
|
+
logLevel: "error",
|
|
70
|
+
failOnWarn: false,
|
|
71
|
+
report: false,
|
|
72
|
+
deps: { neverBundle: true }
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
export { defineConfig };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { BadHookInput } from "./errors.js";
|
|
2
|
+
import { writeStdout } from "./run.js";
|
|
3
|
+
import { commandBypassesVerify } from "./policy.js";
|
|
4
|
+
import { NodeRuntime, NodeServices } from "@effect/platform-node";
|
|
5
|
+
import { Effect, Result, Schema, Stream } from "effect";
|
|
6
|
+
import { Stdio } from "effect/Stdio";
|
|
7
|
+
// src/cursor-hook.ts
|
|
8
|
+
const HookInput = Schema.Struct({ command: Schema.optionalKey(Schema.String) });
|
|
9
|
+
const deny = JSON.stringify({
|
|
10
|
+
permission: "deny",
|
|
11
|
+
user_message: "pnpm verify cannot be skipped.",
|
|
12
|
+
agent_message: "Run pnpm verify. Do not pass --no-verify, a short -n on commit, LEFTHOOK=0, HUSKY=0, VP_GIT_HOOKS=0, VITE_GIT_HOOKS=0, or vp hooks disable."
|
|
13
|
+
});
|
|
14
|
+
const allow = JSON.stringify({ permission: "allow" });
|
|
15
|
+
const hook = Effect.gen(function* () {
|
|
16
|
+
const stdio = yield* Stdio;
|
|
17
|
+
const input = yield* Stream.mkString(Stream.decodeText(stdio.stdin));
|
|
18
|
+
const decoded = Schema.decodeResult(Schema.fromJsonString(HookInput))(input);
|
|
19
|
+
if (Result.isFailure(decoded)) return yield* new BadHookInput({});
|
|
20
|
+
const command = decoded.success.command ?? "";
|
|
21
|
+
if (command === "" || commandBypassesVerify(command)) {
|
|
22
|
+
yield* writeStdout(deny);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
yield* writeStdout(allow);
|
|
26
|
+
});
|
|
27
|
+
NodeRuntime.runMain(hook.pipe(Effect.provide(NodeServices.layer)), { disableErrorReporting: true });
|
|
28
|
+
export {};
|
package/dist/doctor.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Effect, FileSystem } from "effect";
|
|
2
|
+
export declare const doctor: (cwd: string) => Effect.Effect<{
|
|
3
|
+
ok: boolean;
|
|
4
|
+
text: string;
|
|
5
|
+
}, import("effect/PlatformError").PlatformError, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | FileSystem.FileSystem>;
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { findBin, runTool, succeeded } from "./run.js";
|
|
2
|
+
import { sameScalars, scalarEntries } from "./workspace.js";
|
|
3
|
+
import { driftLines } from "./verify.js";
|
|
4
|
+
import { Effect, FileSystem, Result, Schema } from "effect";
|
|
5
|
+
// src/doctor.ts
|
|
6
|
+
const Engines = Schema.Struct({
|
|
7
|
+
node: Schema.String,
|
|
8
|
+
pnpm: Schema.String
|
|
9
|
+
});
|
|
10
|
+
const Manifest = Schema.Struct({ engines: Engines });
|
|
11
|
+
const StateSettings = Schema.Struct({ overrides: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)) });
|
|
12
|
+
const WorkspaceState = Schema.Struct({ settings: Schema.optionalKey(StateSettings) });
|
|
13
|
+
const decodeManifest = Schema.decodeResult(Schema.fromJsonString(Manifest));
|
|
14
|
+
const decodeState = Schema.decodeResult(Schema.fromJsonString(WorkspaceState));
|
|
15
|
+
const nodeEngine = /^>=(\d+)\.(\d+)\.(\d+)$/u;
|
|
16
|
+
const exactVersion = /^v?(\d+)\.(\d+)\.(\d+)$/u;
|
|
17
|
+
const versionOf = (text) => {
|
|
18
|
+
const matched = exactVersion.exec(text.trim());
|
|
19
|
+
const major = matched?.[1];
|
|
20
|
+
const minor = matched?.[2];
|
|
21
|
+
const patch = matched?.[3];
|
|
22
|
+
if (major === void 0 || minor === void 0 || patch === void 0) return;
|
|
23
|
+
return {
|
|
24
|
+
major: Number(major),
|
|
25
|
+
minor: Number(minor),
|
|
26
|
+
patch: Number(patch)
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
const atLeast = (actual, required) => {
|
|
30
|
+
if (actual.major !== required.major) return actual.major > required.major;
|
|
31
|
+
if (actual.minor !== required.minor) return actual.minor > required.minor;
|
|
32
|
+
return actual.patch >= required.patch;
|
|
33
|
+
};
|
|
34
|
+
const pair = (key, value) => [key, value];
|
|
35
|
+
const recordPairs = (record) => {
|
|
36
|
+
const entries = [];
|
|
37
|
+
for (const [key, value] of Object.entries(record)) entries.push(pair(key, value));
|
|
38
|
+
return entries;
|
|
39
|
+
};
|
|
40
|
+
const typescriptPackage = (start) => Effect.gen(function* () {
|
|
41
|
+
const fs = yield* FileSystem.FileSystem;
|
|
42
|
+
const segments = start.split("/");
|
|
43
|
+
let index = segments.length;
|
|
44
|
+
while (index > 1) {
|
|
45
|
+
index -= 1;
|
|
46
|
+
const candidate = `${segments.slice(0, index).join("/")}/node_modules/typescript`;
|
|
47
|
+
if (yield* fs.exists(candidate)) return candidate;
|
|
48
|
+
}
|
|
49
|
+
return "";
|
|
50
|
+
});
|
|
51
|
+
const nativeBinary = (typescriptDir) => Effect.gen(function* () {
|
|
52
|
+
const fs = yield* FileSystem.FileSystem;
|
|
53
|
+
const name = `typescript-${process.platform}-${process.arch}`;
|
|
54
|
+
const binaryName = process.platform === "win32" ? "tsc.exe" : "tsc";
|
|
55
|
+
const parent = typescriptDir.split("/").slice(0, -1).join("/");
|
|
56
|
+
const links = [`${typescriptDir}/node_modules/@typescript/${name}`, `${parent}/@typescript/${name}`];
|
|
57
|
+
for (const link of links) if (yield* fs.exists(link)) return `${yield* fs.realPath(link)}/lib/${binaryName}`;
|
|
58
|
+
return "";
|
|
59
|
+
});
|
|
60
|
+
const patchedBinary = (tscBin) => Effect.gen(function* () {
|
|
61
|
+
const fs = yield* FileSystem.FileSystem;
|
|
62
|
+
const at = tscBin.lastIndexOf("/node_modules/.bin/");
|
|
63
|
+
if (at <= 0) return "";
|
|
64
|
+
const linked = yield* typescriptPackage(tscBin.slice(0, at));
|
|
65
|
+
if (linked === "") return "";
|
|
66
|
+
return yield* nativeBinary(yield* fs.realPath(linked));
|
|
67
|
+
});
|
|
68
|
+
const enginesCheck = () => Effect.gen(function* () {
|
|
69
|
+
const text = yield* (yield* FileSystem.FileSystem).readFileString(`${import.meta.dirname}/../package.json`);
|
|
70
|
+
const decoded = decodeManifest(text);
|
|
71
|
+
if (Result.isFailure(decoded)) return ["package.json:1: engines are missing"];
|
|
72
|
+
const nodeRequired = nodeEngine.exec(decoded.success.engines.node);
|
|
73
|
+
const nodeMajor = nodeRequired?.[1];
|
|
74
|
+
const nodeMinor = nodeRequired?.[2];
|
|
75
|
+
const nodePatch = nodeRequired?.[3];
|
|
76
|
+
const nodeActual = versionOf(process.version);
|
|
77
|
+
const lines = [];
|
|
78
|
+
if (nodeMajor === void 0 || nodeMinor === void 0 || nodePatch === void 0 || nodeActual === void 0 || !atLeast(nodeActual, {
|
|
79
|
+
major: Number(nodeMajor),
|
|
80
|
+
minor: Number(nodeMinor),
|
|
81
|
+
patch: Number(nodePatch)
|
|
82
|
+
})) lines.push(`node:1: ${process.version} does not satisfy ${decoded.success.engines.node}; install Node ${decoded.success.engines.node}`);
|
|
83
|
+
else lines.push(`node: ${process.version} matches ${decoded.success.engines.node}`);
|
|
84
|
+
const pnpm = yield* runTool({
|
|
85
|
+
bin: "pnpm",
|
|
86
|
+
args: ["--version"],
|
|
87
|
+
cwd: import.meta.dirname
|
|
88
|
+
});
|
|
89
|
+
const pnpmActual = succeeded(pnpm.exitCode) ? versionOf(pnpm.stdout) : void 0;
|
|
90
|
+
const pnpmRequired = versionOf(decoded.success.engines.pnpm);
|
|
91
|
+
if (pnpmActual === void 0 || pnpmRequired === void 0 || pnpmActual.major !== pnpmRequired.major || pnpmActual.minor !== pnpmRequired.minor || pnpmActual.patch !== pnpmRequired.patch) lines.push(`pnpm:1: ${pnpm.stdout.trim() || "missing"} does not match ${decoded.success.engines.pnpm}; install pnpm ${decoded.success.engines.pnpm}`);
|
|
92
|
+
else lines.push(`pnpm: ${pnpm.stdout.trim()} matches ${decoded.success.engines.pnpm}`);
|
|
93
|
+
return lines;
|
|
94
|
+
});
|
|
95
|
+
const hooksCheck = (cwd) => Effect.gen(function* () {
|
|
96
|
+
const git = yield* runTool({
|
|
97
|
+
bin: "git",
|
|
98
|
+
args: ["rev-parse", "--git-dir"],
|
|
99
|
+
cwd
|
|
100
|
+
});
|
|
101
|
+
if (!succeeded(git.exitCode)) return ["hooks: not a git repository; vp hooks enable skipped"];
|
|
102
|
+
const hooksPath = yield* runTool({
|
|
103
|
+
bin: "git",
|
|
104
|
+
args: [
|
|
105
|
+
"config",
|
|
106
|
+
"--get",
|
|
107
|
+
"core.hooksPath"
|
|
108
|
+
],
|
|
109
|
+
cwd
|
|
110
|
+
});
|
|
111
|
+
if (!succeeded(hooksPath.exitCode)) return ["git:1: core.hooksPath is unset; run pnpm exec vp hooks enable"];
|
|
112
|
+
const path = hooksPath.stdout.trim();
|
|
113
|
+
if (path === ".vite-hooks" || path === `${cwd}/.vite-hooks` || path.endsWith("/.vite-hooks")) return [`hooks: core.hooksPath is ${path}`];
|
|
114
|
+
return [`hooks: core.hooksPath is ${path}; vp hooks enable skipped`];
|
|
115
|
+
});
|
|
116
|
+
const lockCheck = (cwd) => Effect.gen(function* () {
|
|
117
|
+
const fs = yield* FileSystem.FileSystem;
|
|
118
|
+
const workspacePath = `${cwd}/pnpm-workspace.yaml`;
|
|
119
|
+
if (!(yield* fs.exists(workspacePath))) return ["pnpm-workspace.yaml:1: missing; run aas init"];
|
|
120
|
+
const workspace = yield* fs.readFileString(workspacePath);
|
|
121
|
+
const expected = scalarEntries(workspace, "overrides");
|
|
122
|
+
if (expected === void 0) return ["pnpm-workspace.yaml:1: overrides are missing; run aas sync"];
|
|
123
|
+
if (!(yield* fs.exists(`${cwd}/pnpm-lock.yaml`))) return ["pnpm-lock.yaml:1: missing lockfile; run pnpm install"];
|
|
124
|
+
const lock = yield* fs.readFileString(`${cwd}/pnpm-lock.yaml`);
|
|
125
|
+
const locked = scalarEntries(lock, "overrides");
|
|
126
|
+
if (locked === void 0 || !sameScalars(expected, locked)) return ["pnpm-lock.yaml:1: overrides are stale; run pnpm install"];
|
|
127
|
+
if (!(yield* fs.exists(`${cwd}/node_modules`))) return ["node_modules:1: missing; run pnpm install"];
|
|
128
|
+
const statePath = `${cwd}/node_modules/.pnpm-workspace-state-v1.json`;
|
|
129
|
+
if (!(yield* fs.exists(statePath))) return ["node_modules/.pnpm-workspace-state-v1.json:1: missing install state; run pnpm install"];
|
|
130
|
+
const stateText = yield* fs.readFileString(statePath);
|
|
131
|
+
const state = decodeState(stateText);
|
|
132
|
+
if (Result.isFailure(state)) return ["node_modules/.pnpm-workspace-state-v1.json:1: unreadable install state; run pnpm install"];
|
|
133
|
+
const overrides = state.success.settings?.overrides;
|
|
134
|
+
const installed = overrides === void 0 ? [] : recordPairs(overrides);
|
|
135
|
+
if (!sameScalars(expected, installed)) return ["node_modules/.pnpm-workspace-state-v1.json:1: overrides are stale; run pnpm install"];
|
|
136
|
+
return ["lockfile: overrides match pnpm-workspace.yaml"];
|
|
137
|
+
});
|
|
138
|
+
const tscCheck = () => Effect.gen(function* () {
|
|
139
|
+
const fs = yield* FileSystem.FileSystem;
|
|
140
|
+
const tsc = yield* findBin("tsc").pipe(Effect.catchTag("CommandFailed", () => Effect.succeed("")));
|
|
141
|
+
if (tsc === "") return ["tsc:1: missing executable tsc"];
|
|
142
|
+
const binary = yield* patchedBinary(tsc);
|
|
143
|
+
if (binary === "" || !(yield* fs.exists(`${binary}.original`))) return ["tsc:1: TypeScript is not patched with @effect/tsgo; run pnpm exec effect-tsgo patch"];
|
|
144
|
+
return ["tsc: patched"];
|
|
145
|
+
});
|
|
146
|
+
const failed = (line) => /^\S+:\d+:/u.test(line);
|
|
147
|
+
const doctor = (cwd) => Effect.gen(function* () {
|
|
148
|
+
const lines = [
|
|
149
|
+
...yield* enginesCheck(),
|
|
150
|
+
...yield* hooksCheck(cwd),
|
|
151
|
+
...yield* lockCheck(cwd),
|
|
152
|
+
...yield* tscCheck()
|
|
153
|
+
];
|
|
154
|
+
const drift = yield* driftLines(cwd);
|
|
155
|
+
if (drift.length === 0) lines.push("drift: managed files match the templates");
|
|
156
|
+
else lines.push(...drift);
|
|
157
|
+
return {
|
|
158
|
+
ok: lines.every((line) => !failed(line)),
|
|
159
|
+
text: lines.join("\n")
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
export { doctor };
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
declare const CommandFailed_base: Schema.Class<CommandFailed, Schema.TaggedStruct<"CommandFailed", {
|
|
3
|
+
readonly lines: Schema.$Array<Schema.String>;
|
|
4
|
+
}>, import("effect/Cause").YieldableError & {
|
|
5
|
+
readonly brand: unique symbol;
|
|
6
|
+
}>;
|
|
7
|
+
export declare class CommandFailed extends CommandFailed_base {}
|
|
8
|
+
declare const BadHookInput_base: Schema.Class<BadHookInput, Schema.TaggedStruct<"BadHookInput", {}>, import("effect/Cause").YieldableError & {
|
|
9
|
+
readonly brand: unique symbol;
|
|
10
|
+
}>;
|
|
11
|
+
export declare class BadHookInput extends BadHookInput_base {}
|
|
12
|
+
export {};
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
// src/errors.ts
|
|
3
|
+
var CommandFailed = class extends Schema.TaggedError()("CommandFailed", { lines: Schema.Array(Schema.String) }) {};
|
|
4
|
+
var BadHookInput = class extends Schema.TaggedError()("BadHookInput", {}) {};
|
|
5
|
+
export { BadHookInput, CommandFailed };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { CommandFailed } from "./errors.js";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
export declare const explanation: (names: ReadonlyArray<string>, rule: string | undefined) => string;
|
|
4
|
+
export declare const explain: (rule: string | undefined) => Effect.Effect<undefined, CommandFailed | import("effect/PlatformError").PlatformError, import("effect/Stdio").Stdio>;
|
package/dist/explain.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { CommandFailed } from "./errors.js";
|
|
2
|
+
import { writeStdout } from "./run.js";
|
|
3
|
+
import { ruleNames } from "./plugin/aas.js";
|
|
4
|
+
import { Effect } from "effect";
|
|
5
|
+
// src/explain.ts
|
|
6
|
+
const texts = {
|
|
7
|
+
"effect-tests": "Imports from vitest and focused, skipped, or todo tests hide failures from the gate. Import assert, describe, and it from @effect/vitest and run it.effect.",
|
|
8
|
+
"no-bypass-comment": "A directive comment can silence the diagnostic it sits on. Delete @ts-ignore, @ts-expect-error, @ts-nocheck, eslint-disable, eslint-enable, oxlint-disable, and oxlint-enable. Fix the code.",
|
|
9
|
+
"no-comments": "Comments rot next to the code they describe. Use a clearer name, or write the explanation in AGENTS.md, an ADR, or the README. A shebang is not a comment.",
|
|
10
|
+
"no-effect-run": "Effect.runPromise, Effect.runSync, Effect.runFork, and Effect.runPromiseExit leave the Effect channel. Stay inside Effect. Tests use it.effect. NodeRuntime.runMain is the process boundary.",
|
|
11
|
+
"no-error-class": "new Error and the other built-in error constructors are untyped failures. Define a Schema.TaggedError and pass the brand parameter.",
|
|
12
|
+
"no-explicit-any": "any turns the checker off. Keep the value unknown until a Schema decodes it.",
|
|
13
|
+
"no-module-let": "Module-scope let and var are shared mutable state. Keep that state on a Context.Service. A binding inside a function is fine.",
|
|
14
|
+
"no-non-null-assertion": "A non-null assertion hides a missing value. Narrow the value, or decode it with Schema.",
|
|
15
|
+
"no-plain-class": "A plain class is a second object style. Extend Schema.TaggedError or Context.Service.",
|
|
16
|
+
"no-process-env": "process.env, process[\"env\"], and process?.env skip the config boundary. Declare a supplied value in .env.schema and read it with Config. A computed fallback, such as a homedir path, is Config.String(key).pipe(Config.orElse(() => Config.succeed(value))). That fallback is not Config.withDefault, and the key does not have to be in .env.schema. process.env stays banned.",
|
|
17
|
+
"no-raw-io": "Node fs, http, https, child_process, global fetch, and console bypass Effect's services. Use FileSystem, HttpClient, ChildProcess, and Console. node:net, net, node:tls, and tls have no Effect client in Effect 4.0.6, so those four specifiers are allowed only in a file whose name ends with .adapter.ts. That file is the Node TCP and TLS entry. The same file still bans every other raw import. No command grants an exception.",
|
|
18
|
+
"no-throw": "throw leaves the Effect channel. Yield a Schema.TaggedError.",
|
|
19
|
+
"no-try": "try and catch leave the Effect channel. Failures stay on the Effect error channel.",
|
|
20
|
+
"no-type-assertion": "A type assertion, including as const and an angle-bracket assertion, invents a type. Parse with Schema, or keep a tuple with a const type parameter."
|
|
21
|
+
};
|
|
22
|
+
const formatRule = (name) => {
|
|
23
|
+
const text = texts[name];
|
|
24
|
+
if (text === void 0) return "";
|
|
25
|
+
return `aas/${name}\n${text}`;
|
|
26
|
+
};
|
|
27
|
+
const explanation = (names, rule) => {
|
|
28
|
+
if (rule === void 0 || rule === "") {
|
|
29
|
+
const blocks = [];
|
|
30
|
+
for (const name of names) {
|
|
31
|
+
const block = formatRule(name);
|
|
32
|
+
if (block === "") return "";
|
|
33
|
+
blocks.push(block);
|
|
34
|
+
}
|
|
35
|
+
return blocks.join("\n\n");
|
|
36
|
+
}
|
|
37
|
+
const name = rule.startsWith("aas/") ? rule.slice(4) : rule;
|
|
38
|
+
return formatRule(name);
|
|
39
|
+
};
|
|
40
|
+
const explain = (rule) => Effect.gen(function* () {
|
|
41
|
+
const text = explanation(ruleNames, rule);
|
|
42
|
+
if (text === "") {
|
|
43
|
+
const line = rule === void 0 || rule === "" ? "explain:1: missing text for a rule" : `explain:1: unknown rule ${rule}`;
|
|
44
|
+
return yield* new CommandFailed({ lines: [line] });
|
|
45
|
+
}
|
|
46
|
+
yield* writeStdout(text);
|
|
47
|
+
});
|
|
48
|
+
export { explain, explanation };
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { CommandFailed } from "./errors.js";
|
|
2
|
+
import { Effect, FileSystem } from "effect";
|
|
3
|
+
export declare const ensureVerifyScript: (cwd: string) => Effect.Effect<undefined, CommandFailed | import("effect/PlatformError").PlatformError, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | FileSystem.FileSystem>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { CommandFailed } from "./errors.js";
|
|
2
|
+
import { findBin, runTool, succeeded } from "./run.js";
|
|
3
|
+
import { Effect, FileSystem, Result, Schema } from "effect";
|
|
4
|
+
// src/package-script.ts
|
|
5
|
+
const Manifest = Schema.Record(Schema.String, Schema.Unknown);
|
|
6
|
+
const Scripts = Schema.Record(Schema.String, Schema.String);
|
|
7
|
+
const decodeManifest = Schema.decodeResult(Schema.fromJsonString(Manifest));
|
|
8
|
+
const decodeScripts = Schema.decodeUnknownResult(Scripts);
|
|
9
|
+
const verifyScript = "aas verify";
|
|
10
|
+
const readScripts = (value) => Effect.gen(function* () {
|
|
11
|
+
if (value === void 0) return {};
|
|
12
|
+
const parsed = decodeScripts(value);
|
|
13
|
+
if (Result.isFailure(parsed)) return yield* new CommandFailed({ lines: ["package.json:1: scripts must be a map of strings"] });
|
|
14
|
+
return parsed.success;
|
|
15
|
+
});
|
|
16
|
+
const ensureVerifyScript = (cwd) => Effect.gen(function* () {
|
|
17
|
+
const fs = yield* FileSystem.FileSystem;
|
|
18
|
+
const path = `${cwd}/package.json`;
|
|
19
|
+
if (!(yield* fs.exists(path))) return;
|
|
20
|
+
const text = yield* fs.readFileString(path);
|
|
21
|
+
const decoded = decodeManifest(text);
|
|
22
|
+
if (Result.isFailure(decoded)) return yield* new CommandFailed({ lines: ["package.json:1: package.json is not an object"] });
|
|
23
|
+
const manifest = decoded.success;
|
|
24
|
+
const scripts = yield* readScripts(manifest["scripts"]);
|
|
25
|
+
if (scripts["verify"] !== void 0) return;
|
|
26
|
+
const next = {
|
|
27
|
+
...manifest,
|
|
28
|
+
scripts: {
|
|
29
|
+
...scripts,
|
|
30
|
+
verify: verifyScript
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
yield* fs.writeFileString(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
34
|
+
const vp = yield* findBin("vp");
|
|
35
|
+
const formatted = yield* runTool({
|
|
36
|
+
bin: vp,
|
|
37
|
+
args: ["fmt", "package.json"],
|
|
38
|
+
cwd
|
|
39
|
+
});
|
|
40
|
+
if (!succeeded(formatted.exitCode)) return yield* new CommandFailed({ lines: ["package.json:1: format"] });
|
|
41
|
+
});
|
|
42
|
+
export { ensureVerifyScript };
|