@drkmttr/adhere 0.2.1 → 0.2.2

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.
@@ -1,4 +1,4 @@
1
- import type { Config } from "./src/index.ts";
1
+ import type { Config } from "../src/index.ts";
2
2
 
3
3
  // Type-only import: erased at runtime, so the compiled binary loads this file
4
4
  // without resolving the package (see README, "Configuration").
@@ -16,4 +16,12 @@ jobs:
16
16
  - run: bun install --frozen-lockfile
17
17
  - run: bun run typecheck
18
18
  - run: bun run test
19
- - run: bun run build
19
+ - run: bun run build:npm
20
+ - name: Run the launcher against the linux-x64 package
21
+ run: |
22
+ # Outside the checkout, where the launcher has no source to fall back to.
23
+ launcher="$RUNNER_TEMP/launcher"
24
+ mkdir -p "$launcher/node_modules/@drkmttr"
25
+ cp -r bin "$launcher/bin"
26
+ ln -s "$PWD/dist/npm/linux-x64" "$launcher/node_modules/@drkmttr/adhere-linux-x64"
27
+ node "$launcher/bin/adhere.js" --version
@@ -1,8 +1,18 @@
1
1
  name: Publish
2
2
 
3
3
  on:
4
- release:
5
- types: [published]
4
+ workflow_call:
5
+ inputs:
6
+ tag_name:
7
+ description: "Release tag to publish, for example v0.3.0."
8
+ required: true
9
+ type: string
10
+ workflow_dispatch:
11
+ inputs:
12
+ tag_name:
13
+ description: "Release tag to publish, for example v0.3.0."
14
+ required: true
15
+ type: string
6
16
 
7
17
  permissions:
8
18
  id-token: write
@@ -23,15 +33,36 @@ jobs:
23
33
  node-version: "24"
24
34
  registry-url: "https://registry.npmjs.org"
25
35
  - name: Verify release tag matches package version
36
+ env:
37
+ TAG_NAME: ${{ inputs.tag_name }}
26
38
  run: |
27
- version="${GITHUB_REF_NAME#v}"
39
+ version="${TAG_NAME#v}"
40
+ if [[ "$version" == "$TAG_NAME" || ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-.+][0-9A-Za-z.-]+)?$ ]]; then
41
+ echo "Release tags must look like vX.Y.Z, optionally with a prerelease/build suffix." >&2
42
+ exit 1
43
+ fi
28
44
  package_version="$(node -p "require('./package.json').version")"
29
45
  if [ "$version" != "$package_version" ]; then
30
- echo "Release tag $GITHUB_REF_NAME does not match package.json version $package_version." >&2
46
+ echo "Release tag $TAG_NAME does not match package.json version $package_version." >&2
31
47
  exit 1
32
48
  fi
33
49
  - run: bun install --frozen-lockfile
34
50
  - run: bun run typecheck
35
51
  - run: bun run test
36
- - run: bun run build
37
- - run: npm publish --access public --provenance
52
+ - run: bun run build:npm
53
+ - name: Publish the platform packages
54
+ run: |
55
+ for directory in dist/npm/*/; do
56
+ name="$(node -p "require('./${directory}package.json').name")"
57
+ version="$(node -p "require('./${directory}package.json').version")"
58
+ # A rerun after a partial publish skips the versions already on npm.
59
+ if npm view "$name@$version" version >/dev/null 2>&1; then
60
+ echo "$name@$version is already published."
61
+ else
62
+ npm publish "$directory" --access public --provenance
63
+ fi
64
+ done
65
+ - name: Publish @drkmttr/adhere with the platform packages as optional dependencies
66
+ run: |
67
+ bun scripts/npm-packages.ts pin
68
+ npm publish --access public --provenance
@@ -55,8 +55,17 @@ jobs:
55
55
  - run: bun install --frozen-lockfile
56
56
  - run: bun run typecheck
57
57
  - run: bun run test
58
- - run: bun run build
58
+ - run: bun run build:npm
59
59
  - name: Create GitHub release
60
60
  run: bun run release -- --ci "${{ steps.release.outputs.version }}"
61
61
  env:
62
62
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
63
+
64
+ publish:
65
+ needs: release
66
+ uses: ./.github/workflows/publish.yaml
67
+ permissions:
68
+ contents: read
69
+ id-token: write
70
+ with:
71
+ tag_name: ${{ github.ref_name }}
package/README.md CHANGED
@@ -14,8 +14,8 @@ Deterministic rules (substring matches, type checks) belong in a normal linter.
14
14
  A config file in the working directory of the repo being audited, at one of
15
15
  these paths (keep one):
16
16
 
17
+ - `.adhere/config.ts`, the default, next to the rules
17
18
  - `adhere.config.ts`
18
- - `.adhere/config.ts`
19
19
  - `.adhere.config.ts`
20
20
 
21
21
  ```ts
@@ -46,17 +46,26 @@ package installed, `defineConfig({...})` does the same thing.
46
46
 
47
47
  ### Rules as Markdown files
48
48
 
49
- A repo's own rules live in `.adhere/`, one `*.md` file per rule. The path
50
- without `.md` is the rule id, so `.adhere/data/brand-ports.md` is
51
- `data/brand-ports`. When that directory exists, it is read without any config.
52
- A repo that already has a `docs/` directory may prefer `docs/adhere/`, so the
53
- rules sit with the rest of its documentation; point `rules` at it:
49
+ A repo's own rules live in `.adhere/`, one `*.md` file per rule, next to the
50
+ config and the cache (neither is read as a rule). The path without `.md` is the
51
+ rule id, so `.adhere/data/brand-ports.md` is `data/brand-ports`. When that
52
+ directory exists, it is read without any config.
53
+
54
+ `.adhere/` is the default rather than `docs/adhere/` because it keeps
55
+ everything adhere owns in one directory: the config and the cache are tool
56
+ state, not documentation, and would stay in `.adhere/` anyway. A nested
57
+ `.adhere/` also scopes its rules to the directory that contains it (below). The
58
+ cost is visibility: a dot directory is hidden from `ls`, and from `rg` without
59
+ `--hidden`. A repo that would rather keep its rules with the rest of its
60
+ documentation can point `rules` at `docs/adhere/`:
54
61
 
55
62
  ```ts
56
63
  export default { presets: ["effect"], rules: "./docs/adhere" } satisfies Config;
57
64
  ```
58
65
 
59
- A config is otherwise needed only to name presets or set thresholds.
66
+ Rules read through `rules` apply project-wide; nested `.adhere/` directories
67
+ are not read then. A config is otherwise needed only to name presets or set
68
+ thresholds.
60
69
 
61
70
  Nested `.adhere/` directories are also discovered. A rule in
62
71
  `packages/api/.adhere/data/brand-ports.md` has the same id,
@@ -100,7 +109,7 @@ A preset has the shape of a config without `presets`: `rules`, and optionally
100
109
  [effect-solutions](https://github.com/kitlangton/effect-solutions) docs and the
101
110
  [effect/platform](https://effect.website/docs/platform/introduction/) docs.
102
111
  Name it in the config, or on the command line with `--preset effect`, in which
103
- case `adhere.config.ts` is optional.
112
+ case the config file is optional.
104
113
 
105
114
  ### Precedence
106
115
 
@@ -109,6 +118,19 @@ order (a later preset wins), then the defaults `jev-latest` and `0.7`. A rule
109
118
  in `rules` replaces a preset rule with the same id. A rule's own `threshold`
110
119
  beats all of the above for that rule.
111
120
 
121
+ ## Install
122
+
123
+ ```sh
124
+ npm install --global @drkmttr/adhere # or: bun add --global @drkmttr/adhere
125
+ ```
126
+
127
+ `adhere` is the native executable (below), compiled for macOS and Linux on
128
+ arm64 and x64 and for Windows on x64. Each platform's is its own package,
129
+ `@drkmttr/adhere-<platform>-<arch>`, and an optional dependency of
130
+ `@drkmttr/adhere` that the install fetches only on that platform. The package's
131
+ `bin/adhere.js` runs it with Node, so neither Bun nor adhere's runtime
132
+ dependencies are needed.
133
+
112
134
  ## Run
113
135
 
114
136
  From a checkout, with Bun:
@@ -119,6 +141,9 @@ bun link # puts `adhere` on PATH
119
141
  adhere --preset effect
120
142
  ```
121
143
 
144
+ In a checkout no platform package is installed, so `bin/adhere.js` runs
145
+ `src/main.ts` with Bun instead.
146
+
122
147
  `adhere` audits the working directory. When that directory contains `agents/`,
123
148
  `apps/`, or `packages/`, only those trees are read. Exit code 1 when there is
124
149
  at least one finding.
@@ -151,7 +176,9 @@ are treated as intentional shadowing rather than contradictions.
151
176
  [`--compile`](https://bun.sh/docs/bundler/executables) with
152
177
  `--asset ./presets`. It runs without Bun or `node_modules` on the target
153
178
  machine and still loads the repo's `config.ts` and Markdown rules from disk.
154
- For another platform, add `--target`, for example `bun-darwin-arm64`.
179
+ `bun run build:npm` compiles it for every published platform instead, each
180
+ into its package under `dist/npm/`
181
+ ([`scripts/npm-packages.ts`](./scripts/npm-packages.ts)).
155
182
 
156
183
  ## Release
157
184
 
@@ -168,10 +195,18 @@ derives `0.3.0` from `v0.3.0`, and runs `bun run release -- --ci 0.3.0`.
168
195
  Release-it bumps `package.json`, commits `chore: release v0.3.0` back to
169
196
  `main`, skips npm publish, and creates the GitHub Release for the existing tag.
170
197
 
171
- Publishing stays in `.github/workflows/publish.yaml`: the published GitHub
172
- Release triggers the OIDC trusted-publisher job, which verifies that
173
- `package.json` matches the release tag and then runs
174
- `npm publish --access public --provenance`. No npm token is used.
198
+ Publishing stays in `.github/workflows/publish.yaml`: after release-it
199
+ finishes, `.github/workflows/release.yaml` calls the reusable publish workflow
200
+ directly, avoiding a chained GitHub Release event created by `GITHUB_TOKEN`.
201
+ The publish workflow verifies that `package.json` matches the release tag,
202
+ builds the platform packages, and publishes each of them before
203
+ `@drkmttr/adhere`, which it first lists them in as `optionalDependencies` at
204
+ the same version. Every publish is `npm publish --access public --provenance`.
205
+ No npm token is used, so a new platform package has to be published once by
206
+ hand and given the trusted publisher on npmjs.com before the job can publish
207
+ it. The publish workflow can also be run manually for an already-created
208
+ release tag if a publish needs to be retried; it skips platform packages whose
209
+ version is already on npm.
175
210
 
176
211
  ## How a file is judged
177
212
 
package/bin/adhere.js ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `adhere` on PATH. Runs the executable compiled for this machine, which the
4
+ * install fetched as the optional dependency `@drkmttr/adhere-<platform>-<arch>`
5
+ * (see scripts/npm-packages.ts). A checkout has none installed, so there it
6
+ * runs src/main.ts with Bun instead.
7
+ */
8
+ import { spawnSync } from "node:child_process";
9
+ import { existsSync } from "node:fs";
10
+ import { createRequire } from "node:module";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ const platform = `${process.platform}-${process.arch}`;
14
+ const platformPackage = `@drkmttr/adhere-${platform}`;
15
+ const executable = process.platform === "win32" ? "adhere.exe" : "adhere";
16
+ const args = process.argv.slice(2);
17
+
18
+ const resolveExecutable = () => {
19
+ try {
20
+ return createRequire(import.meta.url).resolve(`${platformPackage}/bin/${executable}`);
21
+ } catch {
22
+ return undefined;
23
+ }
24
+ };
25
+
26
+ const run = (command, commandArgs) => {
27
+ const result = spawnSync(command, commandArgs, { stdio: "inherit" });
28
+ if (result.error) throw result.error;
29
+ process.exitCode = result.status ?? 1;
30
+ };
31
+
32
+ const prebuilt = resolveExecutable();
33
+ if (prebuilt) run(prebuilt, args);
34
+ else if (existsSync(new URL("../.git", import.meta.url)))
35
+ run("bun", [fileURLToPath(new URL("../src/main.ts", import.meta.url)), ...args]);
36
+ else {
37
+ console.error(
38
+ `adhere: ${platformPackage} is not installed, so there is no executable for ${platform}.` +
39
+ " It is an optional dependency of @drkmttr/adhere: reinstall without --omit=optional or --no-optional." +
40
+ " If it is still missing, adhere does not ship an executable for this platform.",
41
+ );
42
+ process.exitCode = 1;
43
+ }
package/bun.lock CHANGED
@@ -5,10 +5,10 @@
5
5
  "": {
6
6
  "name": "@drkmttr/adhere",
7
7
  "dependencies": {
8
- "@effect/platform-bun": "4.0.0-rc.112",
9
8
  "effect": "4.0.0-rc.112",
10
9
  },
11
10
  "devDependencies": {
11
+ "@effect/platform-bun": "4.0.0-rc.112",
12
12
  "@types/bun": "^1.4.2",
13
13
  "@types/node": "^24.13.6",
14
14
  "release-it": "^21.1.0",
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@drkmttr/adhere",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "private": false,
5
- "description": "Lints a repository against reference code in adhere.config.ts, judged by Jev.",
5
+ "description": "Lints a repository against reference code in .adhere/, judged by Jev.",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/darkmatter/adhere.git"
9
9
  },
10
10
  "bin": {
11
- "adhere": "src/main.ts"
11
+ "adhere": "bin/adhere.js"
12
12
  },
13
13
  "type": "module",
14
14
  "imports": {
@@ -26,13 +26,14 @@
26
26
  "test": "vp test",
27
27
  "typecheck": "tsc --noEmit && bun run lint",
28
28
  "build": "bun build --compile --minify --sourcemap src/main.ts --asset ./presets --outfile dist/adhere",
29
+ "build:npm": "bun scripts/npm-packages.ts build",
29
30
  "release": "release-it"
30
31
  },
31
32
  "dependencies": {
32
- "@effect/platform-bun": "4.0.0-rc.112",
33
33
  "effect": "4.0.0-rc.112"
34
34
  },
35
35
  "devDependencies": {
36
+ "@effect/platform-bun": "4.0.0-rc.112",
36
37
  "@types/bun": "^1.4.2",
37
38
  "@types/node": "^24.13.6",
38
39
  "release-it": "^21.1.0",
@@ -58,5 +59,12 @@
58
59
  "allowSameVersion": true,
59
60
  "publish": false
60
61
  }
62
+ },
63
+ "optionalDependencies": {
64
+ "@drkmttr/adhere-darwin-arm64": "0.2.2",
65
+ "@drkmttr/adhere-darwin-x64": "0.2.2",
66
+ "@drkmttr/adhere-linux-arm64": "0.2.2",
67
+ "@drkmttr/adhere-linux-x64": "0.2.2",
68
+ "@drkmttr/adhere-win32-x64": "0.2.2"
61
69
  }
62
70
  }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The npm packages that carry the compiled executable, one per platform. Each
3
+ * is an optional dependency of `@drkmttr/adhere` limited by `os` and `cpu`, so
4
+ * an install fetches only its own machine's, and `bin/adhere.js` runs it.
5
+ *
6
+ * bun scripts/npm-packages.ts build compile every platform into dist/npm/
7
+ * bun scripts/npm-packages.ts pin list them in package.json at its version
8
+ *
9
+ * Only the publish job runs `pin`. The committed package.json leaves them out
10
+ * because a version is not on npm until that job has published it.
11
+ */
12
+ import packageJson from "../package.json";
13
+ import { $ } from "bun";
14
+ import { rm } from "node:fs/promises";
15
+
16
+ process.chdir(new URL("../", import.meta.url).pathname);
17
+
18
+ /** `os` and `cpu` are Node's `process.platform` and `process.arch`, which the launcher names the package from. */
19
+ const platforms = [
20
+ { os: "darwin", cpu: "arm64", target: "bun-darwin-arm64" },
21
+ { os: "darwin", cpu: "x64", target: "bun-darwin-x64" },
22
+ { os: "linux", cpu: "arm64", target: "bun-linux-arm64" },
23
+ { os: "linux", cpu: "x64", target: "bun-linux-x64" },
24
+ { os: "win32", cpu: "x64", target: "bun-windows-x64" },
25
+ ] as const;
26
+
27
+ type Platform = (typeof platforms)[number];
28
+
29
+ const packageName = ({ os, cpu }: Platform) => `${packageJson.name}-${os}-${cpu}`;
30
+
31
+ const writeJson = (path: string, value: unknown) =>
32
+ Bun.write(path, `${JSON.stringify(value, null, 2)}\n`);
33
+
34
+ const build = async () => {
35
+ for (const platform of platforms) {
36
+ const directory = `dist/npm/${platform.os}-${platform.cpu}`;
37
+ const executable = platform.os === "win32" ? "adhere.exe" : "adhere";
38
+ await rm(directory, { recursive: true, force: true });
39
+ await $`bun build --compile --minify --sourcemap --target=${platform.target} src/main.ts --asset ./presets --outfile ${directory}/bin/${executable}`;
40
+ await writeJson(`${directory}/package.json`, {
41
+ name: packageName(platform),
42
+ version: packageJson.version,
43
+ description: `The adhere executable for ${platform.os}-${platform.cpu}. Install ${packageJson.name}, which depends on it.`,
44
+ repository: packageJson.repository,
45
+ os: [platform.os],
46
+ cpu: [platform.cpu],
47
+ files: [`bin/${executable}`],
48
+ preferUnplugged: true,
49
+ publishConfig: packageJson.publishConfig,
50
+ });
51
+ }
52
+ };
53
+
54
+ const pin = () =>
55
+ writeJson("package.json", {
56
+ ...packageJson,
57
+ optionalDependencies: Object.fromEntries(
58
+ platforms.map((platform) => [packageName(platform), packageJson.version]),
59
+ ),
60
+ });
61
+
62
+ const command = Bun.argv[2];
63
+ if (command === "build") await build();
64
+ else if (command === "pin") await pin();
65
+ else {
66
+ console.error("usage: bun scripts/npm-packages.ts build|pin");
67
+ process.exit(2);
68
+ }
@@ -44,9 +44,11 @@ Skip rules a preset already covers. Run `adhere --help` and read
44
44
 
45
45
  ## 3. Write each rule as a file
46
46
 
47
- Default location `.adhere/<topic>/<slug>.md`. If the repo already has `docs/`,
48
- use `docs/adhere/<topic>/<slug>.md` and set `rules: "./docs/adhere"` in the
49
- config. The path without `.md` is the rule id.
47
+ Default location `.adhere/<topic>/<slug>.md`, next to the config and the
48
+ cache. Use `docs/adhere/<topic>/<slug>.md` with `rules: "./docs/adhere"` in the
49
+ config only when the user wants rules kept with the repo's docs; rules read
50
+ that way apply project-wide, and nested `.adhere/` directories are not read.
51
+ The path without `.md` is the rule id.
50
52
 
51
53
  ````md
52
54
  ---
@@ -73,10 +75,10 @@ Rules for the rule:
73
75
 
74
76
  ## 4. Configure
75
77
 
76
- Pick one config location: `adhere.config.ts`, `.adhere/config.ts`, or
77
- `.adhere.config.ts`. A config is optional when `.adhere/` exists or a preset is
78
- passed on the command line. Use a type-only import so the file also loads
79
- under the native binary:
78
+ Put the config at `.adhere/config.ts`. `adhere.config.ts` and
79
+ `.adhere.config.ts` also load; keep one. A config is optional when `.adhere/`
80
+ exists or a preset is passed on the command line. Use a type-only import so
81
+ the file also loads under the native binary:
80
82
 
81
83
  ```ts
82
84
  import type { Config } from "@drkmttr/adhere";
package/src/cli.ts CHANGED
@@ -48,7 +48,7 @@ class UsageReported extends Schema.TaggedError<UsageReported>()(
48
48
 
49
49
  const preset = Flag.choice("preset", presetNames).pipe(
50
50
  Flag.optional,
51
- Flag.withDescription("Add a built-in rule set. With a preset, adhere.config.ts is optional."),
51
+ Flag.withDescription("Add a built-in rule set. With a preset, .adhere/config.ts is optional."),
52
52
  );
53
53
 
54
54
  const threshold = Flag.float("threshold").pipe(
@@ -86,7 +86,7 @@ const audit = Command.make("adhere", { preset, threshold }, () =>
86
86
  }),
87
87
  ).pipe(
88
88
  Command.withDescription(
89
- "Audit the working directory against reference code from adhere.config.ts or a preset, judged by Jev.",
89
+ "Audit the working directory against reference code from .adhere/config.ts or a preset, judged by Jev.",
90
90
  ),
91
91
  Command.provide((input) =>
92
92
  auditLayer({
@@ -98,7 +98,7 @@ const audit = Command.make("adhere", { preset, threshold }, () =>
98
98
 
99
99
  const INIT_HELP = `Usage: adhere init [--force]
100
100
 
101
- Scaffold adhere.config.ts and example Markdown rules.
101
+ Scaffold .adhere/config.ts and example Markdown rules.
102
102
 
103
103
  Options:
104
104
  --force Overwrite existing scaffold files
package/src/config.ts CHANGED
@@ -32,6 +32,12 @@ export const AdhereConfig = Schema.Struct({
32
32
  /** Where a repo keeps its rule files by default. The cache lives under it. */
33
33
  export const ADHERE_DIRECTORY = ".adhere";
34
34
  export const CACHE_DIRECTORY = `${ADHERE_DIRECTORY}/cache`;
35
+ /** Where a config may live, relative to the working directory. Exactly one may exist. */
36
+ export const CONFIG_FILES = [
37
+ `${ADHERE_DIRECTORY}/config.ts`,
38
+ "adhere.config.ts",
39
+ ".adhere.config.ts",
40
+ ] as const;
35
41
  export interface AdhereConfig extends Schema.Schema.Type<typeof AdhereConfig> {}
36
42
 
37
43
  /** The shape a config file default-exports. */
package/src/init.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { access, mkdir, writeFile } from "node:fs/promises";
2
2
  import { dirname, join, relative } from "node:path";
3
+ import { CONFIG_FILES } from "#config.ts";
3
4
  import { Effect } from "effect";
4
5
 
5
6
  export interface InitResult {
@@ -44,8 +45,10 @@ export const loadCustomerProfile = (customerId: CustomerId) =>
44
45
  \`\`\`
45
46
  `;
46
47
 
48
+ const CONFIG_PATH = CONFIG_FILES[0];
49
+
47
50
  const files = [
48
- ["adhere.config.ts", CONFIG],
51
+ [CONFIG_PATH, CONFIG],
49
52
  [".adhere/style/prefer-small-files.md", SMALL_FILES],
50
53
  [".adhere/style/name-domain-actions.md", NAME_EFFECTS],
51
54
  ] as const;
@@ -56,6 +59,15 @@ const exists = (path: string): Promise<boolean> =>
56
59
  () => false,
57
60
  );
58
61
 
62
+ /** A config at another accepted path. Scaffolding a second one would refuse every run. */
63
+ const otherConfig = (root: string) =>
64
+ Effect.promise(async () => {
65
+ for (const file of CONFIG_FILES) {
66
+ if (file !== CONFIG_PATH && (await exists(join(root, file)))) return file;
67
+ }
68
+ return undefined;
69
+ });
70
+
59
71
  const writeScaffoldFile = (root: string, path: string, contents: string, options: InitOptions) =>
60
72
  Effect.tryPromise({
61
73
  try: async () => {
@@ -79,7 +91,12 @@ export const initProject = (
79
91
  Effect.gen(function* () {
80
92
  const created: Array<string> = [];
81
93
  const skipped: Array<string> = [];
94
+ const existing = yield* otherConfig(root);
82
95
  for (const [path, contents] of files) {
96
+ if (path === CONFIG_PATH && existing !== undefined) {
97
+ skipped.push(existing);
98
+ continue;
99
+ }
83
100
  const status = yield* writeScaffoldFile(root, path, contents, options);
84
101
  if (status === "created") {
85
102
  created.push(relative(root, join(root, path)));
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  ADHERE_DIRECTORY,
3
3
  type AdhereConfig as Decoded,
4
+ CONFIG_FILES,
4
5
  ConfigUnavailable,
5
6
  decodeConfig,
6
7
  type Loaded,
@@ -20,9 +21,6 @@ import {
20
21
  } from "#rules.ts";
21
22
  import { Context, Effect, FileSystem, Layer, Path, Record } from "effect";
22
23
 
23
- /** Where a config may live, relative to the working directory. Exactly one may exist. */
24
- export const CONFIG_FILES = ["adhere.config.ts", ".adhere/config.ts", ".adhere.config.ts"] as const;
25
-
26
24
  export class AdhereConfig extends Context.Service<
27
25
  AdhereConfig,
28
26
  ResolvedConfig
@@ -1,4 +1,4 @@
1
- import { readFile } from "node:fs/promises";
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { Crypto, Effect, FileSystem, Layer, Path, Record } from "effect";
@@ -433,7 +433,7 @@ describe("init", () => {
433
433
  it("scaffolds config and example rules without clobbering existing files", async () => {
434
434
  const root = join(tmpdir(), `adhere-init-${Date.now()}`);
435
435
  await Effect.runPromise(initProject(root));
436
- const config = await readFile(join(root, "adhere.config.ts"), "utf8");
436
+ const config = await readFile(join(root, ".adhere", "config.ts"), "utf8");
437
437
  const rule = await readFile(join(root, ".adhere", "style", "prefer-small-files.md"), "utf8");
438
438
 
439
439
  expect(config).toContain("satisfies Config");
@@ -441,9 +441,21 @@ describe("init", () => {
441
441
 
442
442
  const second = await Effect.runPromise(initProject(root));
443
443
  expect(second.created).toEqual([]);
444
- expect(second.skipped).toContain("adhere.config.ts");
444
+ expect(second.skipped).toContain(".adhere/config.ts");
445
445
  expect(second.skipped).toContain(".adhere/style/prefer-small-files.md");
446
446
  });
447
+
448
+ it("keeps a config at another accepted path instead of adding a second one", async () => {
449
+ const root = join(tmpdir(), `adhere-init-existing-${Date.now()}`);
450
+ await mkdir(root, { recursive: true });
451
+ await writeFile(join(root, "adhere.config.ts"), "export default {};\n", "utf8");
452
+
453
+ const result = await Effect.runPromise(initProject(root, { force: true }));
454
+
455
+ expect(result.skipped).toContain("adhere.config.ts");
456
+ expect(result.created).not.toContain(".adhere/config.ts");
457
+ await expect(access(join(root, ".adhere", "config.ts"))).rejects.toThrow();
458
+ });
447
459
  });
448
460
 
449
461
  describe("pipeline", () => {
package/tsconfig.json CHANGED
@@ -21,6 +21,6 @@
21
21
  "#*": ["./src/*"]
22
22
  }
23
23
  },
24
- "include": ["src/**/*.ts", "test/**/*.ts", "adhere.config.ts"],
24
+ "include": ["src/**/*.ts", "test/**/*.ts", "scripts/**/*.ts", ".adhere/config.ts"],
25
25
  "exclude": ["dist", "node_modules"]
26
26
  }