@drkmttr/adhere 0.12.0 → 0.13.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/README.md +194 -51
- package/package.json +9 -9
- package/presets/effect/basics/{instrument-with-pipe.md → instrument-with-pipe/RULE.md} +1 -1
- package/presets/effect/config/{business-logic-depends-on-config-service.md → business-logic-depends-on-config-service/RULE.md} +3 -3
- package/presets/effect/config/{secrets-are-redacted.md → secrets-are-redacted/RULE.md} +3 -3
- package/presets/effect/config/{validate-with-schema.md → validate-with-schema/RULE.md} +4 -4
- package/skills/adhere/SKILL.md +213 -118
- package/skills/adhere-fix/SKILL.md +9 -8
- package/skills/adhere-setup/SKILL.md +244 -0
- package/src/cli.ts +49 -33
- package/src/config.ts +64 -3
- package/src/index.ts +2 -0
- package/src/init.ts +36 -25
- package/src/install.ts +37 -26
- package/src/main.ts +1 -1
- package/src/markdown.ts +9 -1
- package/src/rules.ts +135 -56
- package/src/services/AdhereConfig.ts +15 -3
- package/src/services/Credentials.ts +4 -4
- package/src/services/Jev.http.ts +82 -10
- package/src/services/Jev.ts +41 -10
- package/src/workflows/audit.ts +33 -14
- package/src/workflows/format.ts +172 -35
- package/src/workspaces.ts +141 -0
- /package/presets/alchemy/apis/{match-remote-errors-by-tag.md → match-remote-errors-by-tag/RULE.md} +0 -0
- /package/presets/alchemy/apis/{schemas-at-trust-boundaries.md → schemas-at-trust-boundaries/RULE.md} +0 -0
- /package/presets/alchemy/data/{commit-migrations-then-deploy.md → commit-migrations-then-deploy/RULE.md} +0 -0
- /package/presets/alchemy/data/{hyperdrive-direct-origin.md → hyperdrive-direct-origin/RULE.md} +0 -0
- /package/presets/alchemy/data/{migrations-use-direct-url.md → migrations-use-direct-url/RULE.md} +0 -0
- /package/presets/alchemy/data/{never-disable-tls-verification.md → never-disable-tls-verification/RULE.md} +0 -0
- /package/presets/alchemy/data/{private-endpoint-at-runtime.md → private-endpoint-at-runtime/RULE.md} +0 -0
- /package/presets/alchemy/data/{single-migration-owner.md → single-migration-owner/RULE.md} +0 -0
- /package/presets/alchemy/durable/{idempotent-event-handlers.md → idempotent-event-handlers/RULE.md} +0 -0
- /package/presets/alchemy/durable/{io-inside-steps.md → io-inside-steps/RULE.md} +0 -0
- /package/presets/alchemy/durable/{no-self-triggering-bucket-writes.md → no-self-triggering-bucket-writes/RULE.md} +0 -0
- /package/presets/alchemy/durable/{stable-step-names.md → stable-step-names/RULE.md} +0 -0
- /package/presets/alchemy/durable/{webhook-state-is-truth.md → webhook-state-is-truth/RULE.md} +0 -0
- /package/presets/alchemy/providers/{diff-guards-unresolved-props.md → diff-guards-unresolved-props/RULE.md} +0 -0
- /package/presets/alchemy/providers/{idempotent-delete.md → idempotent-delete/RULE.md} +0 -0
- /package/presets/alchemy/providers/{lazy-layer-construction.md → lazy-layer-construction/RULE.md} +0 -0
- /package/presets/alchemy/providers/{reconcile-from-observed-state.md → reconcile-from-observed-state/RULE.md} +0 -0
- /package/presets/alchemy/resources/{anchor-paths-with-import-meta.md → anchor-paths-with-import-meta/RULE.md} +0 -0
- /package/presets/alchemy/resources/{idempotent-actions-and-seeds.md → idempotent-actions-and-seeds/RULE.md} +0 -0
- /package/presets/alchemy/resources/{previews-reference-shared-resources.md → previews-reference-shared-resources/RULE.md} +0 -0
- /package/presets/alchemy/resources/{protect-irreplaceable-data.md → protect-irreplaceable-data/RULE.md} +0 -0
- /package/presets/alchemy/resources/{remote-state-for-ci.md → remote-state-for-ci/RULE.md} +0 -0
- /package/presets/alchemy/resources/{stable-logical-ids.md → stable-logical-ids/RULE.md} +0 -0
- /package/presets/alchemy/resources/{stage-unique-physical-names.md → stage-unique-physical-names/RULE.md} +0 -0
- /package/presets/alchemy/runtime/{bind-tag-not-implementation.md → bind-tag-not-implementation/RULE.md} +0 -0
- /package/presets/alchemy/runtime/{deterministic-config-defaults.md → deterministic-config-defaults/RULE.md} +0 -0
- /package/presets/alchemy/runtime/{disposables-in-request-scope.md → disposables-in-request-scope/RULE.md} +0 -0
- /package/presets/alchemy/runtime/{lambda-no-post-response-work.md → lambda-no-post-response-work/RULE.md} +0 -0
- /package/presets/alchemy/runtime/{no-per-request-work-at-construction.md → no-per-request-work-at-construction/RULE.md} +0 -0
- /package/presets/alchemy/runtime/{resolve-config-and-bindings-at-construction.md → resolve-config-and-bindings-at-construction/RULE.md} +0 -0
- /package/presets/alchemy/secrets/{let-alchemy-mint-infra-secrets.md → let-alchemy-mint-infra-secrets/RULE.md} +0 -0
- /package/presets/alchemy/secrets/{none-in-browser-env.md → none-in-browser-env/RULE.md} +0 -0
- /package/presets/alchemy/secrets/{redacted-until-use.md → redacted-until-use/RULE.md} +0 -0
- /package/presets/alchemy/secrets/{unwrap-before-compare-or-send.md → unwrap-before-compare-or-send/RULE.md} +0 -0
- /package/presets/alchemy/security/{authorize-public-endpoints.md → authorize-public-endpoints/RULE.md} +0 -0
- /package/presets/alchemy/security/{github-secret-not-variable.md → github-secret-not-variable/RULE.md} +0 -0
- /package/presets/alchemy/security/{identity-from-verified-session.md → identity-from-verified-session/RULE.md} +0 -0
- /package/presets/alchemy/security/{keep-auth-protections-on.md → keep-auth-protections-on/RULE.md} +0 -0
- /package/presets/alchemy/security/{least-privilege-bindings.md → least-privilege-bindings/RULE.md} +0 -0
- /package/presets/alchemy/security/{least-privilege-ci-credentials.md → least-privilege-ci-credentials/RULE.md} +0 -0
- /package/presets/alchemy/security/{lookup-failure-is-not-anonymous.md → lookup-failure-is-not-anonymous/RULE.md} +0 -0
- /package/presets/effect/basics/{gen-for-sequencing.md → gen-for-sequencing/RULE.md} +0 -0
- /package/presets/effect/config/{tests-provide-values-directly.md → tests-provide-values-directly/RULE.md} +0 -0
- /package/presets/effect/data/{brand-meaningful-primitives.md → brand-meaningful-primitives/RULE.md} +0 -0
- /package/presets/effect/data/{variants-are-tagged-unions.md → variants-are-tagged-unions/RULE.md} +0 -0
- /package/presets/effect/errors/{catch-defects-at-boundaries-only.md → catch-defects-at-boundaries-only/RULE.md} +0 -0
- /package/presets/effect/errors/{defects-for-bugs.md → defects-for-bugs/RULE.md} +0 -0
- /package/presets/effect/errors/{wrap-external-errors.md → wrap-external-errors/RULE.md} +0 -0
- /package/presets/effect/services/{dependencies-through-layers.md → dependencies-through-layers/RULE.md} +0 -0
- /package/presets/effect/services/{fresh-layer-per-test.md → fresh-layer-per-test/RULE.md} +0 -0
- /package/presets/effect/services/{memoize-parameterized-layers.md → memoize-parameterized-layers/RULE.md} +0 -0
- /package/presets/effect/services/{no-mutable-state.md → no-mutable-state/RULE.md} +0 -0
- /package/presets/effect/services/{operations-have-no-requirements.md → operations-have-no-requirements/RULE.md} +0 -0
- /package/presets/effect/services/{test-layers-are-in-memory.md → test-layers-are-in-memory/RULE.md} +0 -0
- /package/presets/effect/testing/{test-clock-for-time.md → test-clock-for-time/RULE.md} +0 -0
- /package/presets/react/effects/{clean-up-what-they-start.md → clean-up-what-they-start/RULE.md} +0 -0
- /package/presets/react/effects/{event-logic-in-handlers.md → event-logic-in-handlers/RULE.md} +0 -0
- /package/presets/react/effects/{external-stores-use-sync-external-store.md → external-stores-use-sync-external-store/RULE.md} +0 -0
- /package/presets/react/effects/{fetches-ignore-stale-responses.md → fetches-ignore-stale-responses/RULE.md} +0 -0
- /package/presets/react/effects/{notify-parents-in-handlers.md → notify-parents-in-handlers/RULE.md} +0 -0
- /package/presets/react/server/{no-private-data-to-client.md → no-private-data-to-client/RULE.md} +0 -0
- /package/presets/react/server/{server-functions-authorize-and-validate.md → server-functions-authorize-and-validate/RULE.md} +0 -0
- /package/presets/react/state/{no-contradictory-state.md → no-contradictory-state/RULE.md} +0 -0
- /package/presets/react/state/{no-props-copied-into-state.md → no-props-copied-into-state/RULE.md} +0 -0
- /package/presets/react/state/{store-ids-not-copies.md → store-ids-not-copies/RULE.md} +0 -0
- /package/presets/security/{constant-time-secret-compare.md → constant-time-secret-compare/RULE.md} +0 -0
- /package/presets/security/{no-secrets-in-output.md → no-secrets-in-output/RULE.md} +0 -0
- /package/presets/security/{no-untrusted-input-in-commands-or-paths.md → no-untrusted-input-in-commands-or-paths/RULE.md} +0 -0
- /package/presets/security/{parameterized-queries.md → parameterized-queries/RULE.md} +0 -0
- /package/presets/typescript/async/{concurrent-independent-work.md → concurrent-independent-work/RULE.md} +0 -0
- /package/presets/typescript/async/{network-calls-have-timeouts.md → network-calls-have-timeouts/RULE.md} +0 -0
- /package/presets/typescript/design/{no-argument-mutation.md → no-argument-mutation/RULE.md} +0 -0
- /package/presets/typescript/errors/{no-swallowed-errors.md → no-swallowed-errors/RULE.md} +0 -0
- /package/presets/typescript/errors/{release-on-every-path.md → release-on-every-path/RULE.md} +0 -0
- /package/presets/typescript/testing/{assert-outcomes.md → assert-outcomes/RULE.md} +0 -0
- /package/presets/typescript/testing/{independent-tests.md → independent-tests/RULE.md} +0 -0
- /package/presets/typescript/types/{assertions-only-when-proven.md → assertions-only-when-proven/RULE.md} +0 -0
- /package/presets/typescript/types/{derive-dont-restate.md → derive-dont-restate/RULE.md} +0 -0
- /package/presets/typescript/types/{unrepresentable-invalid-states.md → unrepresentable-invalid-states/RULE.md} +0 -0
- /package/presets/typescript/types/{validate-external-data.md → validate-external-data/RULE.md} +0 -0
package/src/init.ts
CHANGED
|
@@ -28,9 +28,13 @@ export interface InitOptions {
|
|
|
28
28
|
* install`, rather than one that lints itself.
|
|
29
29
|
*/
|
|
30
30
|
readonly shared?: string;
|
|
31
|
+
/** Who can see the shared repo alchemy creates. Defaults to public. */
|
|
32
|
+
readonly visibility?: Visibility;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
export type Visibility = "public" | "private";
|
|
36
|
+
|
|
37
|
+
// adhere-ignore-file import-written-files -- src/index.ts exports initProject, so a Bun text import here breaks loading the package on Node and in Vitest (checked 2026-09-26), and the compiled executable has no files beside it to read.
|
|
34
38
|
const CONFIG = `import { defineConfig } from "@drkmttr/adhere";
|
|
35
39
|
|
|
36
40
|
export default defineConfig({
|
|
@@ -39,10 +43,11 @@ export default defineConfig({
|
|
|
39
43
|
`;
|
|
40
44
|
|
|
41
45
|
/**
|
|
42
|
-
* A project for the config
|
|
43
|
-
* without it the editor opens
|
|
44
|
-
*
|
|
45
|
-
* and `nodenext`
|
|
46
|
+
* A project for the config and any rules written in TypeScript. A tsconfig's
|
|
47
|
+
* globs skip dot directories, so without it the editor opens
|
|
48
|
+
* `.adhere/config.ts` outside any project, where it cannot resolve
|
|
49
|
+
* `@drkmttr/adhere`, whose types only `bundler`, `node16`, and `nodenext`
|
|
50
|
+
* resolution reach.
|
|
46
51
|
*/
|
|
47
52
|
const TSCONFIG = `{
|
|
48
53
|
"compilerOptions": {
|
|
@@ -54,7 +59,7 @@ const TSCONFIG = `{
|
|
|
54
59
|
"skipLibCheck": true,
|
|
55
60
|
"allowImportingTsExtensions": true
|
|
56
61
|
},
|
|
57
|
-
"include": ["
|
|
62
|
+
"include": ["**/*.ts"]
|
|
58
63
|
}
|
|
59
64
|
`;
|
|
60
65
|
|
|
@@ -113,8 +118,9 @@ export const handle = (id: string) =>
|
|
|
113
118
|
const sharedReadme = (org: string, repo: string) => `# ${repo}
|
|
114
119
|
|
|
115
120
|
Shared [adhere](https://github.com/darkmatter/adhere) rules for ${org}'s
|
|
116
|
-
repositories. Each rule is a
|
|
117
|
-
|
|
121
|
+
repositories. Each rule is a directory in \`.adhere/rules/\` that holds a
|
|
122
|
+
\`RULE.md\`, or a \`RULE.ts\`, and a repo that copies it in has Jev judge its
|
|
123
|
+
files against it when it runs \`adhere lint\`.
|
|
118
124
|
|
|
119
125
|
## Use these rules
|
|
120
126
|
|
|
@@ -127,13 +133,14 @@ adhere install ${org}/${repo}/style
|
|
|
127
133
|
adhere install ${org}/${repo}/style/prefer-small-files
|
|
128
134
|
\`\`\`
|
|
129
135
|
|
|
130
|
-
The copies land in \`.adhere/${org}/${repo}/\` and are that repo's own from then
|
|
136
|
+
The copies land in \`.adhere/rules/${org}/${repo}/\` and are that repo's own from then
|
|
131
137
|
on, to edit or to start other rules from. To take a later version of a rule,
|
|
132
138
|
install it again with \`--force\`, which overwrites local edits.
|
|
133
139
|
|
|
134
140
|
## Add a rule
|
|
135
141
|
|
|
136
|
-
Add a
|
|
142
|
+
Add a directory under \`.adhere/rules/<topic>/\` with the rule in a \`RULE.md\`;
|
|
143
|
+
its path is the rule's id. See
|
|
137
144
|
[Rules as Markdown files](https://github.com/darkmatter/adhere#rules-as-markdown-files)
|
|
138
145
|
and the [rule writing tips](https://github.com/darkmatter/adhere#rule-writing-tips).
|
|
139
146
|
\`adhere validate\` checks each rule's wording and asks Jev whether any two
|
|
@@ -172,7 +179,11 @@ jobs:
|
|
|
172
179
|
TYPESAFE_API_KEY: \${{ secrets.TYPESAFE_API_KEY }}
|
|
173
180
|
`;
|
|
174
181
|
|
|
175
|
-
const sharedStack = (
|
|
182
|
+
const sharedStack = (
|
|
183
|
+
org: string,
|
|
184
|
+
repo: string,
|
|
185
|
+
visibility: Visibility,
|
|
186
|
+
) => `import * as Alchemy from "alchemy";
|
|
176
187
|
import * as GitHub from "alchemy/GitHub";
|
|
177
188
|
import * as Output from "alchemy/Output";
|
|
178
189
|
import * as Config from "effect/Config";
|
|
@@ -188,7 +199,7 @@ export default Alchemy.Stack(
|
|
|
188
199
|
owner,
|
|
189
200
|
name: "${repo}",
|
|
190
201
|
description: "Shared adhere rules",
|
|
191
|
-
visibility: "
|
|
202
|
+
visibility: "${visibility}",
|
|
192
203
|
});
|
|
193
204
|
// CI's adhere validate asks Jev whether any two rules contradict. Naming
|
|
194
205
|
// the repository by its output makes alchemy create it before the secret.
|
|
@@ -232,19 +243,22 @@ const SHARED_GITIGNORE = `node_modules/
|
|
|
232
243
|
|
|
233
244
|
const CONFIG_PATH = CONFIG_FILES[0];
|
|
234
245
|
|
|
235
|
-
/** The
|
|
246
|
+
/** The project of the config at `.adhere/config.ts`, and of the rules written in TypeScript beside it. */
|
|
236
247
|
const CONFIG_PROJECT = ".adhere/tsconfig.json";
|
|
237
248
|
|
|
238
249
|
const EXAMPLES = [
|
|
239
|
-
[".adhere/style/prefer-small-files.md", SMALL_FILES],
|
|
240
|
-
[".adhere/style/name-domain-actions.md", NAME_EFFECTS],
|
|
250
|
+
[".adhere/rules/style/prefer-small-files/RULE.md", SMALL_FILES],
|
|
251
|
+
[".adhere/rules/style/name-domain-actions/RULE.md", NAME_EFFECTS],
|
|
241
252
|
] as const;
|
|
242
253
|
|
|
243
254
|
/**
|
|
244
|
-
* A shared repo has no config: \`adhere install\` copies only
|
|
245
|
-
* \`validate\` reads \`.adhere/\` without one.
|
|
255
|
+
* A shared repo has no config: \`adhere install\` copies only rules, and
|
|
256
|
+
* \`validate\` reads \`.adhere/rules/\` without one.
|
|
246
257
|
*/
|
|
247
|
-
const filesFor = (
|
|
258
|
+
const filesFor = (
|
|
259
|
+
shared: string | undefined,
|
|
260
|
+
visibility: Visibility,
|
|
261
|
+
): ReadonlyArray<readonly [string, string]> => {
|
|
248
262
|
if (shared === undefined) {
|
|
249
263
|
return [[CONFIG_PATH, CONFIG], [CONFIG_PROJECT, TSCONFIG], ...EXAMPLES];
|
|
250
264
|
}
|
|
@@ -252,7 +266,7 @@ const filesFor = (shared: string | undefined): ReadonlyArray<readonly [string, s
|
|
|
252
266
|
return [
|
|
253
267
|
["README.md", sharedReadme(org, repo)],
|
|
254
268
|
[".github/workflows/adhere.yaml", SHARED_WORKFLOW],
|
|
255
|
-
["alchemy.run.ts", sharedStack(org, repo)],
|
|
269
|
+
["alchemy.run.ts", sharedStack(org, repo, visibility)],
|
|
256
270
|
["package.json", sharedPackage(repo)],
|
|
257
271
|
[".gitignore", SHARED_GITIGNORE],
|
|
258
272
|
...EXAMPLES,
|
|
@@ -312,14 +326,11 @@ const packageManagerAt = async (root: string): Promise<PackageManager> => {
|
|
|
312
326
|
|
|
313
327
|
/**
|
|
314
328
|
* pnpm refuses to add to a workspace root without a flag. `-w` fails outside a
|
|
315
|
-
* workspace, so
|
|
316
|
-
* also fails on msgpackr-extract's unapproved build script. It comes with
|
|
317
|
-
* effect, and adhere never calls msgpackr, so the second flag lets pnpm skip
|
|
318
|
-
* the build with a warning.
|
|
329
|
+
* workspace, so this one, which works in both, stands in for it.
|
|
319
330
|
*/
|
|
320
331
|
const addFlags: Record<PackageManager, ReadonlyArray<string>> = {
|
|
321
332
|
bun: [],
|
|
322
|
-
pnpm: ["--ignore-workspace-root-check"
|
|
333
|
+
pnpm: ["--ignore-workspace-root-check"],
|
|
323
334
|
yarn: [],
|
|
324
335
|
npm: [],
|
|
325
336
|
};
|
|
@@ -369,7 +380,7 @@ export const initProject = (
|
|
|
369
380
|
const created: Array<string> = [];
|
|
370
381
|
const skipped: Array<string> = [];
|
|
371
382
|
const existing = options.shared === undefined ? yield* otherConfig(root) : undefined;
|
|
372
|
-
for (const [path, contents] of filesFor(options.shared)) {
|
|
383
|
+
for (const [path, contents] of filesFor(options.shared, options.visibility ?? "public")) {
|
|
373
384
|
if (path === CONFIG_PATH && existing !== undefined) {
|
|
374
385
|
skipped.push(existing);
|
|
375
386
|
continue;
|
package/src/install.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { access,
|
|
2
|
+
import { access, cp, mkdir, mkdtemp, readdir, readFile, rm } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { dirname, join, sep } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { RULES_DIRECTORY, type RuleId } from "#config.ts";
|
|
6
6
|
import { parseRuleMarkdown } from "#markdown.ts";
|
|
7
7
|
import { Effect } from "effect";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* A GitHub repo whose `.adhere/`
|
|
10
|
+
* A GitHub repo whose `.adhere/rules/` another repo copies: `org/repo`, then
|
|
11
11
|
* optionally a topic or a rule's id, then optionally `#` and a branch or tag,
|
|
12
12
|
* as in `darkmatter/standards/data/brand-ports#v3`.
|
|
13
13
|
*/
|
|
@@ -63,43 +63,52 @@ const cloned = (source: Source) =>
|
|
|
63
63
|
(directory) => Effect.promise(() => rm(directory, { recursive: true, force: true })),
|
|
64
64
|
);
|
|
65
65
|
|
|
66
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* The repo's rules in its root `.adhere/rules/`, as lint reads them there:
|
|
68
|
+
* each directory that holds a RULE.md or a RULE.ts, by its path, with that file.
|
|
69
|
+
*/
|
|
67
70
|
const rulesIn = (source: Source, clone: string) =>
|
|
68
71
|
Effect.tryPromise({
|
|
69
72
|
try: async () => {
|
|
70
|
-
const directory = join(clone,
|
|
73
|
+
const directory = join(clone, RULES_DIRECTORY);
|
|
71
74
|
const entries = await readdir(directory, { recursive: true }).catch(() => []);
|
|
72
|
-
const
|
|
73
|
-
.
|
|
74
|
-
|
|
75
|
+
const rules = entries
|
|
76
|
+
.flatMap((entry) => {
|
|
77
|
+
const [, id, name] = /^(.+)\/(RULE\.(?:md|ts))$/.exec(entry.split(sep).join("/")) ?? [];
|
|
78
|
+
return id === undefined || name === undefined ? [] : [{ id: id as RuleId, name }];
|
|
79
|
+
})
|
|
75
80
|
.filter(
|
|
76
|
-
(id) => source.rule === "" || id === source.rule || id.startsWith(`${source.rule}/`),
|
|
81
|
+
({ id }) => source.rule === "" || id === source.rule || id.startsWith(`${source.rule}/`),
|
|
77
82
|
)
|
|
78
|
-
.sort();
|
|
79
|
-
if (
|
|
83
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
84
|
+
if (rules.length === 0) {
|
|
80
85
|
throw new Error(
|
|
81
86
|
source.rule === ""
|
|
82
|
-
? `it has no rules in ${
|
|
83
|
-
: `it has no rule or topic ${source.rule} in ${
|
|
87
|
+
? `it has no rules in ${RULES_DIRECTORY}/`
|
|
88
|
+
: `it has no rule or topic ${source.rule} in ${RULES_DIRECTORY}/`,
|
|
84
89
|
);
|
|
85
90
|
}
|
|
86
|
-
return
|
|
91
|
+
return rules.map(({ id, name }) => ({ id, name, directory: join(directory, id) }));
|
|
87
92
|
},
|
|
88
93
|
catch: failed(`Nothing to copy from ${source.repo}`),
|
|
89
94
|
});
|
|
90
95
|
|
|
91
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* Each rule a source names, with its description. A rule written in
|
|
98
|
+
* TypeScript is listed without one: reading it would run the repo's code.
|
|
99
|
+
*/
|
|
92
100
|
export const listRules = (source: Source) =>
|
|
93
101
|
Effect.scoped(
|
|
94
102
|
Effect.gen(function* () {
|
|
95
103
|
const rules = yield* rulesIn(source, yield* cloned(source));
|
|
96
|
-
return yield* Effect.forEach(rules, ({ id,
|
|
104
|
+
return yield* Effect.forEach(rules, ({ id, name, directory }) =>
|
|
97
105
|
Effect.gen(function* () {
|
|
106
|
+
if (name === "RULE.ts") return { id, description: "(TypeScript, which list does not run)" };
|
|
98
107
|
const text = yield* Effect.tryPromise({
|
|
99
|
-
try: () => readFile(
|
|
108
|
+
try: () => readFile(join(directory, name), "utf8"),
|
|
100
109
|
catch: failed(`Could not read ${id}`),
|
|
101
110
|
});
|
|
102
|
-
const rule = yield* parseRuleMarkdown(text, `${source.repo}/${id}
|
|
111
|
+
const rule = yield* parseRuleMarkdown(text, `${source.repo}/${id}/${name}`);
|
|
103
112
|
return { id, description: rule.description };
|
|
104
113
|
}),
|
|
105
114
|
);
|
|
@@ -112,11 +121,12 @@ export interface InstallResult {
|
|
|
112
121
|
}
|
|
113
122
|
|
|
114
123
|
/**
|
|
115
|
-
* Copies the rules a source names
|
|
116
|
-
*
|
|
117
|
-
* `acme/rules/data/ports`, and two
|
|
118
|
-
*
|
|
119
|
-
*
|
|
124
|
+
* Copies the rules a source names, each its whole directory, helpers and all,
|
|
125
|
+
* into `root`'s `.adhere/rules/` under the source's org and repo, so
|
|
126
|
+
* `data/ports` from acme/rules is the rule `acme/rules/data/ports`, and two
|
|
127
|
+
* sources' rules never collide. They are the project's own rules from then
|
|
128
|
+
* on, and a copied RULE.ts runs whenever lint does. As with init, a rule
|
|
129
|
+
* already there is skipped, unless `force` replaces its directory.
|
|
120
130
|
*/
|
|
121
131
|
export const installRules = (root: string, source: Source, options: { readonly force?: boolean }) =>
|
|
122
132
|
Effect.scoped(
|
|
@@ -124,8 +134,8 @@ export const installRules = (root: string, source: Source, options: { readonly f
|
|
|
124
134
|
const rules = yield* rulesIn(source, yield* cloned(source));
|
|
125
135
|
const created: Array<string> = [];
|
|
126
136
|
const skipped: Array<string> = [];
|
|
127
|
-
for (const { id,
|
|
128
|
-
const path = `${
|
|
137
|
+
for (const { id, directory } of rules) {
|
|
138
|
+
const path = `${RULES_DIRECTORY}/${source.repo}/${id}/`;
|
|
129
139
|
const target = join(root, path);
|
|
130
140
|
const copied = yield* Effect.tryPromise({
|
|
131
141
|
try: async () => {
|
|
@@ -134,8 +144,9 @@ export const installRules = (root: string, source: Source, options: { readonly f
|
|
|
134
144
|
() => false,
|
|
135
145
|
);
|
|
136
146
|
if (exists && options.force !== true) return false;
|
|
147
|
+
await rm(target, { recursive: true, force: true });
|
|
137
148
|
await mkdir(dirname(target), { recursive: true });
|
|
138
|
-
await
|
|
149
|
+
await cp(directory, target, { recursive: true });
|
|
139
150
|
return true;
|
|
140
151
|
},
|
|
141
152
|
catch: failed(`Could not write ${path}`),
|
package/src/main.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { version } from "#version.ts";
|
|
|
5
5
|
import { BunRuntime, BunServices } from "@effect/platform-bun";
|
|
6
6
|
import { plugin } from "bun";
|
|
7
7
|
import { Cause, Effect, Layer, Logger, References, Runtime } from "effect";
|
|
8
|
-
import { Command } from "effect/
|
|
8
|
+
import { Command } from "effect/cli";
|
|
9
9
|
|
|
10
10
|
// A config file may import `@drkmttr/adhere`, for `defineConfig`. The compiled
|
|
11
11
|
// executable resolves no packages from disk, so it answers that import with
|
package/src/markdown.ts
CHANGED
|
@@ -44,6 +44,7 @@ const FrontMatter = Schema.Struct({
|
|
|
44
44
|
level: Schema.optionalKey(Schema.Literals(["error", "warning"])),
|
|
45
45
|
appliesTo: Schema.optionalKey(Matchers),
|
|
46
46
|
excludeIf: Schema.optionalKey(Matchers),
|
|
47
|
+
includeWorkspacePackages: Schema.optionalKey(Schema.Literals(["true", "false"])),
|
|
47
48
|
});
|
|
48
49
|
|
|
49
50
|
const FRONT_MATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
@@ -206,5 +207,12 @@ export const parseRuleMarkdown = (
|
|
|
206
207
|
message: `${file}: the body needs code, in a fence under a Must, Never, Should, or Should not heading, or untagged`,
|
|
207
208
|
});
|
|
208
209
|
}
|
|
209
|
-
|
|
210
|
+
const { includeWorkspacePackages, ...fields } = front;
|
|
211
|
+
return {
|
|
212
|
+
...fields,
|
|
213
|
+
...(includeWorkspacePackages === undefined
|
|
214
|
+
? {}
|
|
215
|
+
: { includeWorkspacePackages: includeWorkspacePackages === "true" }),
|
|
216
|
+
...code,
|
|
217
|
+
};
|
|
210
218
|
});
|
package/src/rules.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ConfigUnavailable,
|
|
3
3
|
type Override,
|
|
4
|
-
|
|
4
|
+
Rule,
|
|
5
5
|
type RuleId,
|
|
6
6
|
type RuleSource,
|
|
7
7
|
SKIPPED_DIRECTORIES,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import { parseRuleMarkdown } from "#markdown.ts";
|
|
10
10
|
import { walkFiles } from "#walk.ts";
|
|
11
11
|
import { clearingStatus, countOf, Status } from "#services/Status.ts";
|
|
12
|
-
import { Effect, FileSystem, Path, Record } from "effect";
|
|
12
|
+
import { Effect, FileSystem, Path, Record, Schema } from "effect";
|
|
13
13
|
|
|
14
14
|
const refused = (directory: string) => (problem: { readonly message: string }) =>
|
|
15
15
|
ConfigUnavailable.make({
|
|
@@ -40,6 +40,10 @@ export type RuleSet = ReadonlyArray<RuleEntry>;
|
|
|
40
40
|
|
|
41
41
|
const ADHERE_SEGMENT = ".adhere";
|
|
42
42
|
const CACHE_SEGMENT = "cache";
|
|
43
|
+
/** Where in a `.adhere/` its rules sit, one to a directory. */
|
|
44
|
+
const RULES_PREFIX = "rules/";
|
|
45
|
+
/** The file in a rule's directory that is the rule: Markdown, or TypeScript that default-exports `defineRule({...})`. */
|
|
46
|
+
const RULE_FILES: ReadonlySet<string> = new Set(["RULE.md", "RULE.ts"]);
|
|
43
47
|
|
|
44
48
|
const normalized = (path: string): string => path.replaceAll("\\", "/");
|
|
45
49
|
|
|
@@ -48,12 +52,69 @@ const segmentsOf = (relative: string): ReadonlyArray<string> =>
|
|
|
48
52
|
.split("/")
|
|
49
53
|
.filter((segment) => segment.length > 0);
|
|
50
54
|
|
|
51
|
-
|
|
55
|
+
/** A rule file, and its path in the directory it was read from. */
|
|
56
|
+
interface RuleFile {
|
|
57
|
+
readonly id: RuleId;
|
|
58
|
+
readonly leaf: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The rules among `leaves`, the paths in `directory`, whose rules sit under
|
|
63
|
+
* `prefix` one to a directory: `<id>/RULE.md` or `<id>/RULE.ts`. Anything else
|
|
64
|
+
* in a rule's directory, such as a helper its RULE.ts imports, is not a rule.
|
|
65
|
+
* A directory with both files refuses, and so does a Markdown file outside
|
|
66
|
+
* every rule's directory, but a README.md: before rule directories, it was a
|
|
67
|
+
* rule, and the refusal says where it goes now to keep its id.
|
|
68
|
+
*/
|
|
69
|
+
const ruleFilesIn = (
|
|
70
|
+
directory: string,
|
|
71
|
+
leaves: ReadonlyArray<string>,
|
|
72
|
+
prefix: string,
|
|
73
|
+
): Effect.Effect<ReadonlyArray<RuleFile>, ConfigUnavailable> => {
|
|
74
|
+
const files = leaves.flatMap((leaf): ReadonlyArray<RuleFile> => {
|
|
75
|
+
const segments = segmentsOf(leaf.slice(prefix.length));
|
|
76
|
+
return leaf.startsWith(prefix) && segments.length > 1 && RULE_FILES.has(segments.at(-1) ?? "")
|
|
77
|
+
? [{ id: segments.slice(0, -1).join("/"), leaf }]
|
|
78
|
+
: [];
|
|
79
|
+
});
|
|
80
|
+
const both = files.find((file, index) => files.findIndex(({ id }) => id === file.id) < index);
|
|
81
|
+
if (both !== undefined) {
|
|
82
|
+
return Effect.fail(
|
|
83
|
+
ConfigUnavailable.make({
|
|
84
|
+
message: `${directory}/${prefix}${both.id}: a rule is a RULE.md or a RULE.ts, not both`,
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const homes = files.map(({ leaf }) => leaf.slice(0, leaf.lastIndexOf("/") + 1));
|
|
89
|
+
const flat = leaves.filter((leaf) => {
|
|
90
|
+
const name = segmentsOf(leaf).at(-1) ?? "";
|
|
91
|
+
return (
|
|
92
|
+
leaf.endsWith(".md") &&
|
|
93
|
+
!RULE_FILES.has(name) &&
|
|
94
|
+
name !== "README.md" &&
|
|
95
|
+
!homes.some((home) => leaf.startsWith(home))
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
if (flat.length > 0) {
|
|
99
|
+
return Effect.fail(
|
|
100
|
+
ConfigUnavailable.make({
|
|
101
|
+
message: [
|
|
102
|
+
`Rules now live one to a directory, as ${directory}/${prefix}<id>/RULE.md or RULE.ts. Move each of these to keep its id:`,
|
|
103
|
+
...flat.map(
|
|
104
|
+
(leaf) =>
|
|
105
|
+
` ${directory}/${leaf} → ${directory}/${prefix}${leaf.slice(0, -".md".length)}/RULE.md`,
|
|
106
|
+
),
|
|
107
|
+
].join("\n"),
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return Effect.succeed(files);
|
|
112
|
+
};
|
|
52
113
|
|
|
53
114
|
/**
|
|
54
115
|
* Whether the rule walk reads into a directory: on the way to a `.adhere/`,
|
|
55
116
|
* not a skipped tree or `.git`; inside one, anything but its cache, since a
|
|
56
|
-
* directory there such as `.adhere/e2e/` is a rule topic.
|
|
117
|
+
* directory there such as `.adhere/rules/e2e/` is a rule topic.
|
|
57
118
|
*/
|
|
58
119
|
const entersForRules = (relative: string): boolean => {
|
|
59
120
|
const segments = segmentsOf(relative);
|
|
@@ -65,45 +126,30 @@ const entersForRules = (relative: string): boolean => {
|
|
|
65
126
|
return name === ADHERE_SEGMENT || !(SKIPPED_DIRECTORIES.has(name) || name === ".git");
|
|
66
127
|
};
|
|
67
128
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return adhere < 0 ? [] : segments.slice(0, adhere);
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const ruleIdOf = (relative: string): RuleId => {
|
|
91
|
-
const rulePath = rulePathOf(segmentsOf(relative));
|
|
92
|
-
const leaf = rulePath.join("/");
|
|
93
|
-
return leaf.slice(0, -".md".length);
|
|
129
|
+
/**
|
|
130
|
+
* Each `.adhere/` the walk found, by its path from the root, with the paths
|
|
131
|
+
* of the files in it. Only the path to a `.adhere/` counts against skipped
|
|
132
|
+
* trees: `.adhere/rules/e2e/` is a rule topic, not a skip.
|
|
133
|
+
*/
|
|
134
|
+
const adhereDirectoriesOf = (
|
|
135
|
+
relatives: ReadonlyArray<string>,
|
|
136
|
+
): ReadonlyMap<string, ReadonlyArray<string>> => {
|
|
137
|
+
const found = new Map<string, Array<string>>();
|
|
138
|
+
for (const relative of relatives) {
|
|
139
|
+
const segments = segmentsOf(relative);
|
|
140
|
+
const adhere = segments.lastIndexOf(ADHERE_SEGMENT);
|
|
141
|
+
if (adhere < 0 || segments.slice(0, adhere).some((name) => SKIPPED_DIRECTORIES.has(name))) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const directory = segments.slice(0, adhere + 1).join("/");
|
|
145
|
+
found.set(directory, [...(found.get(directory) ?? []), segments.slice(adhere + 1).join("/")]);
|
|
146
|
+
}
|
|
147
|
+
return found;
|
|
94
148
|
};
|
|
95
149
|
|
|
96
150
|
const joinPath = (path: Path.Path, root: string, parts: ReadonlyArray<string>): string =>
|
|
97
151
|
parts.reduce((current, part) => path.join(current, part), root);
|
|
98
152
|
|
|
99
|
-
const scopedRule = (path: Path.Path, root: string, relative: string, rule: Rule): RuleEntry => {
|
|
100
|
-
const segments = segmentsOf(relative);
|
|
101
|
-
const scopeParts = scopePathOf(segments);
|
|
102
|
-
const scope = scopeParts.length === 0 ? path.resolve(root) : joinPath(path, root, scopeParts);
|
|
103
|
-
const file = joinPath(path, root, segments);
|
|
104
|
-
return { id: ruleIdOf(relative), file, scope, rule };
|
|
105
|
-
};
|
|
106
|
-
|
|
107
153
|
const isInside = (file: string, scope: string): boolean =>
|
|
108
154
|
file === scope || file.startsWith(`${scope}/`);
|
|
109
155
|
|
|
@@ -111,9 +157,40 @@ const moreSpecific = (a: RuleEntry, b: RuleEntry): RuleEntry =>
|
|
|
111
157
|
normalized(a.scope).length >= normalized(b.scope).length ? a : b;
|
|
112
158
|
|
|
113
159
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
160
|
+
* A rule written in TypeScript: the file's default export, decoded as a
|
|
161
|
+
* config's inline rule is, so a hook such as `appendState` comes with it.
|
|
162
|
+
* Like a config, the file is imported, so its code runs.
|
|
163
|
+
*/
|
|
164
|
+
const importRule = Effect.fn("importRule")(function* (file: string) {
|
|
165
|
+
const path = yield* Path.Path;
|
|
166
|
+
const url = yield* path.toFileUrl(file).pipe(Effect.orDie);
|
|
167
|
+
const loaded = yield* Effect.tryPromise({
|
|
168
|
+
try: () => import(url.href) as Promise<{ readonly default?: unknown }>,
|
|
169
|
+
catch: (cause) =>
|
|
170
|
+
ConfigUnavailable.make({
|
|
171
|
+
message: `Could not load ${file}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
return yield* Schema.decodeUnknownEffect(Rule)(loaded.default).pipe(
|
|
175
|
+
Effect.mapError((problem) =>
|
|
176
|
+
ConfigUnavailable.make({
|
|
177
|
+
message: `${file}: a rule file must default-export defineRule({...}): ${problem.message}`,
|
|
178
|
+
}),
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
/** A rule file's rule. A file that cannot be read refuses with the directory it was read from. */
|
|
184
|
+
const readRule = Effect.fn("readRule")(function* (file: string, directory: string) {
|
|
185
|
+
if (file.endsWith(".ts")) return yield* importRule(file);
|
|
186
|
+
const fs = yield* FileSystem.FileSystem;
|
|
187
|
+
const text = yield* fs.readFileString(file).pipe(Effect.mapError(refused(directory)));
|
|
188
|
+
return yield* parseRuleMarkdown(text, file);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Every rule under `directory`, validated and keyed by its directory's path
|
|
193
|
+
* in it: `basics/gen/RULE.md` is the rule `basics/gen`.
|
|
117
194
|
*/
|
|
118
195
|
export const loadRules = Effect.fn("loadRules")(function* (directory: string) {
|
|
119
196
|
const fs = yield* FileSystem.FileSystem;
|
|
@@ -121,19 +198,18 @@ export const loadRules = Effect.fn("loadRules")(function* (directory: string) {
|
|
|
121
198
|
const entries = yield* fs
|
|
122
199
|
.readDirectory(directory, { recursive: true })
|
|
123
200
|
.pipe(Effect.mapError(refused(directory)));
|
|
124
|
-
const files = entries.
|
|
125
|
-
const rules = yield* Effect.forEach(files, (
|
|
126
|
-
Effect.
|
|
127
|
-
const file = path.join(directory, relative);
|
|
128
|
-
const text = yield* fs.readFileString(file).pipe(Effect.mapError(refused(directory)));
|
|
129
|
-
const id: RuleId = relative.slice(0, -".md".length).split(path.sep).join("/");
|
|
130
|
-
return [id, yield* parseRuleMarkdown(text, file)] as const;
|
|
131
|
-
}),
|
|
201
|
+
const files = yield* ruleFilesIn(directory, entries.map(normalized).sort(), "");
|
|
202
|
+
const rules = yield* Effect.forEach(files, ({ id, leaf }) =>
|
|
203
|
+
Effect.map(readRule(path.join(directory, leaf), directory), (rule) => [id, rule] as const),
|
|
132
204
|
);
|
|
133
205
|
return Record.fromEntries(rules) as Readonly<Record<RuleId, Rule>>;
|
|
134
206
|
});
|
|
135
207
|
|
|
136
|
-
/**
|
|
208
|
+
/**
|
|
209
|
+
* Every rule in a `.adhere/rules/` under `root`, scoped to the directory that
|
|
210
|
+
* holds its `.adhere/`: `packages/api/.adhere/rules/data/ports/RULE.md` is
|
|
211
|
+
* `data/ports` for the files under `packages/api/`.
|
|
212
|
+
*/
|
|
137
213
|
export const loadAdhereRuleSet = Effect.fn("loadAdhereRuleSet")(function* (root: string) {
|
|
138
214
|
const fs = yield* FileSystem.FileSystem;
|
|
139
215
|
const path = yield* Path.Path;
|
|
@@ -145,14 +221,17 @@ export const loadAdhereRuleSet = Effect.fn("loadAdhereRuleSet")(function* (root:
|
|
|
145
221
|
),
|
|
146
222
|
),
|
|
147
223
|
).pipe(Effect.mapError(refused(root)));
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
224
|
+
const found = yield* Effect.forEach(adhereDirectoriesOf(entries), ([adhere, leaves]) =>
|
|
225
|
+
Effect.flatMap(ruleFilesIn(adhere, [...leaves].sort(), RULES_PREFIX), (files) => {
|
|
226
|
+
const owner = segmentsOf(adhere).slice(0, -1);
|
|
227
|
+
const scope = owner.length === 0 ? path.resolve(root) : joinPath(path, root, owner);
|
|
228
|
+
return Effect.forEach(files, ({ id, leaf }) => {
|
|
229
|
+
const file = joinPath(path, root, segmentsOf(`${adhere}/${leaf}`));
|
|
230
|
+
return Effect.map(readRule(file, root), (rule): RuleEntry => ({ id, file, scope, rule }));
|
|
231
|
+
});
|
|
154
232
|
}),
|
|
155
233
|
);
|
|
234
|
+
return found.flat();
|
|
156
235
|
});
|
|
157
236
|
|
|
158
237
|
/** Rules as given, or loaded from the directory a source names. */
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
ADHERE_DIRECTORY,
|
|
3
2
|
AdhereConfig as AdhereConfigSchema,
|
|
4
3
|
CONFIG_FILES,
|
|
5
4
|
ConfigUnavailable,
|
|
@@ -10,6 +9,7 @@ import {
|
|
|
10
9
|
presetsOf,
|
|
11
10
|
type ResolvedConfig,
|
|
12
11
|
resolveConfig,
|
|
12
|
+
RULES_DIRECTORY,
|
|
13
13
|
} from "#config.ts";
|
|
14
14
|
import { type PresetName, presetOf, presets, wholePresetOf } from "#presets.ts";
|
|
15
15
|
import {
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
shownId,
|
|
22
22
|
withOverrides,
|
|
23
23
|
} from "#rules.ts";
|
|
24
|
+
import { workspacePackagesOf } from "#workspaces.ts";
|
|
24
25
|
import { Context, Effect, FileSystem, Layer, Path, Record } from "effect";
|
|
25
26
|
|
|
26
27
|
export class AdhereConfig extends Context.Service<AdhereConfig, ResolvedConfig>()(
|
|
@@ -94,7 +95,7 @@ export const AdhereConfigLive = (flags: Flags) =>
|
|
|
94
95
|
const hasProjectRules = discoveredEntries.length > 0;
|
|
95
96
|
if (configFile === undefined && !hasProjectRules && !hasPreset) {
|
|
96
97
|
return yield* ConfigUnavailable.make({
|
|
97
|
-
message: `Nothing to audit against: no config file (${CONFIG_FILES.join(", ")}), no ${
|
|
98
|
+
message: `Nothing to audit against: no config file (${CONFIG_FILES.join(", ")}), no rules in ${RULES_DIRECTORY}/, and no --preset. Pass --preset effect to use the built-in rules.`,
|
|
98
99
|
});
|
|
99
100
|
}
|
|
100
101
|
const projectEntries: RuleSet =
|
|
@@ -129,6 +130,17 @@ export const AdhereConfigLive = (flags: Flags) =>
|
|
|
129
130
|
});
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
|
-
|
|
133
|
+
const scopedRules = withOverrides(entries, overrides);
|
|
134
|
+
// The manifests are read on every run, so a changed workspace is seen, and only when a rule reads them.
|
|
135
|
+
const workspacePackages = scopedRules.some(
|
|
136
|
+
(entry) => entry.rule.includeWorkspacePackages === true,
|
|
137
|
+
)
|
|
138
|
+
? yield* workspacePackagesOf(cwd)
|
|
139
|
+
: undefined;
|
|
140
|
+
return AdhereConfig.of({
|
|
141
|
+
...resolved,
|
|
142
|
+
scopedRules,
|
|
143
|
+
...(workspacePackages === undefined ? {} : { workspacePackages }),
|
|
144
|
+
});
|
|
133
145
|
}),
|
|
134
146
|
);
|
|
@@ -46,10 +46,10 @@ export const CredentialsLive = Layer.effect(Credentials)(
|
|
|
46
46
|
const fs = yield* FileSystem.FileSystem;
|
|
47
47
|
const path = yield* Path.Path;
|
|
48
48
|
|
|
49
|
-
const file = Config.
|
|
49
|
+
const file = Config.NonEmptyString("XDG_CONFIG_HOME").pipe(
|
|
50
50
|
Config.orElse(() =>
|
|
51
|
-
Config.
|
|
52
|
-
Config.orElse(() => Config.
|
|
51
|
+
Config.NonEmptyString("HOME").pipe(
|
|
52
|
+
Config.orElse(() => Config.NonEmptyString("USERPROFILE")),
|
|
53
53
|
Config.map((home) => path.join(home, ".config")),
|
|
54
54
|
),
|
|
55
55
|
),
|
|
@@ -78,7 +78,7 @@ export const CredentialsLive = Layer.effect(Credentials)(
|
|
|
78
78
|
|
|
79
79
|
// Read when a request needs the key, not before: a cached run needs none.
|
|
80
80
|
const apiKey = Effect.fn("Credentials.apiKey")(function* () {
|
|
81
|
-
const fromEnvironment = yield* Config.option(Config.
|
|
81
|
+
const fromEnvironment = yield* Config.option(Config.Redacted("TYPESAFE_API_KEY")).pipe(
|
|
82
82
|
Effect.mapError((problem) => refused(`Cannot read TYPESAFE_API_KEY: ${problem.message}`)),
|
|
83
83
|
);
|
|
84
84
|
if (Option.isSome(fromEnvironment)) return fromEnvironment.value;
|