habit-hooks 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +233 -0
- package/dist/baseline/commands.d.ts +12 -0
- package/dist/baseline/commands.js +131 -0
- package/dist/baseline/commands.js.map +1 -0
- package/dist/baseline/file-hash.d.ts +2 -0
- package/dist/baseline/file-hash.js +25 -0
- package/dist/baseline/file-hash.js.map +1 -0
- package/dist/baseline/filter.d.ts +8 -0
- package/dist/baseline/filter.js +31 -0
- package/dist/baseline/filter.js.map +1 -0
- package/dist/baseline/store.d.ts +18 -0
- package/dist/baseline/store.js +106 -0
- package/dist/baseline/store.js.map +1 -0
- package/dist/checks/comment-check.d.ts +3 -0
- package/dist/checks/comment-check.js +83 -0
- package/dist/checks/comment-check.js.map +1 -0
- package/dist/checks/eslint-wrap.d.ts +2 -0
- package/dist/checks/eslint-wrap.js +104 -0
- package/dist/checks/eslint-wrap.js.map +1 -0
- package/dist/checks/jscpd-wrap.d.ts +6 -0
- package/dist/checks/jscpd-wrap.js +158 -0
- package/dist/checks/jscpd-wrap.js.map +1 -0
- package/dist/checks/knip-resolve.d.ts +4 -0
- package/dist/checks/knip-resolve.js +49 -0
- package/dist/checks/knip-resolve.js.map +1 -0
- package/dist/checks/knip-schema.d.ts +32 -0
- package/dist/checks/knip-schema.js +24 -0
- package/dist/checks/knip-schema.js.map +1 -0
- package/dist/checks/knip-wrap.d.ts +4 -0
- package/dist/checks/knip-wrap.js +127 -0
- package/dist/checks/knip-wrap.js.map +1 -0
- package/dist/cli/baseline-commands.d.ts +2 -0
- package/dist/cli/baseline-commands.js +61 -0
- package/dist/cli/baseline-commands.js.map +1 -0
- package/dist/cli/emit.d.ts +7 -0
- package/dist/cli/emit.js +8 -0
- package/dist/cli/emit.js.map +1 -0
- package/dist/cli/init/detect.d.ts +8 -0
- package/dist/cli/init/detect.js +20 -0
- package/dist/cli/init/detect.js.map +1 -0
- package/dist/cli/init/git-hook.d.ts +6 -0
- package/dist/cli/init/git-hook.js +48 -0
- package/dist/cli/init/git-hook.js.map +1 -0
- package/dist/cli/init/install-commands.d.ts +6 -0
- package/dist/cli/init/install-commands.js +55 -0
- package/dist/cli/init/install-commands.js.map +1 -0
- package/dist/cli/init/package-scripts.d.ts +6 -0
- package/dist/cli/init/package-scripts.js +55 -0
- package/dist/cli/init/package-scripts.js.map +1 -0
- package/dist/cli/init/prompts.d.ts +9 -0
- package/dist/cli/init/prompts.js +33 -0
- package/dist/cli/init/prompts.js.map +1 -0
- package/dist/cli/init/reporters.d.ts +11 -0
- package/dist/cli/init/reporters.js +40 -0
- package/dist/cli/init/reporters.js.map +1 -0
- package/dist/cli/init/run.d.ts +12 -0
- package/dist/cli/init/run.js +159 -0
- package/dist/cli/init/run.js.map +1 -0
- package/dist/cli/init/scaffold-baseline.d.ts +4 -0
- package/dist/cli/init/scaffold-baseline.js +11 -0
- package/dist/cli/init/scaffold-baseline.js.map +1 -0
- package/dist/cli/init/scaffold-config.d.ts +13 -0
- package/dist/cli/init/scaffold-config.js +42 -0
- package/dist/cli/init/scaffold-config.js.map +1 -0
- package/dist/cli/init/scaffold-eslint-config.d.ts +2 -0
- package/dist/cli/init/scaffold-eslint-config.js +12 -0
- package/dist/cli/init/scaffold-eslint-config.js.map +1 -0
- package/dist/cli/init/scaffold-jscpd-config.d.ts +2 -0
- package/dist/cli/init/scaffold-jscpd-config.js +12 -0
- package/dist/cli/init/scaffold-jscpd-config.js.map +1 -0
- package/dist/cli/init/scaffold-knip-config.d.ts +2 -0
- package/dist/cli/init/scaffold-knip-config.js +12 -0
- package/dist/cli/init/scaffold-knip-config.js.map +1 -0
- package/dist/cli/init/skill.d.ts +7 -0
- package/dist/cli/init/skill.js +36 -0
- package/dist/cli/init/skill.js.map +1 -0
- package/dist/cli/init/snippet.d.ts +1 -0
- package/dist/cli/init/snippet.js +18 -0
- package/dist/cli/init/snippet.js.map +1 -0
- package/dist/cli/init/templates/eslint-config.d.ts +2 -0
- package/dist/cli/init/templates/eslint-config.js +35 -0
- package/dist/cli/init/templates/eslint-config.js.map +1 -0
- package/dist/cli/init/templates/jscpd-config.d.ts +2 -0
- package/dist/cli/init/templates/jscpd-config.js +9 -0
- package/dist/cli/init/templates/jscpd-config.js.map +1 -0
- package/dist/cli/init/templates/knip-config.d.ts +2 -0
- package/dist/cli/init/templates/knip-config.js +8 -0
- package/dist/cli/init/templates/knip-config.js.map +1 -0
- package/dist/cli/init-command.d.ts +2 -0
- package/dist/cli/init-command.js +33 -0
- package/dist/cli/init-command.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +101 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/defaults.d.ts +4 -0
- package/dist/config/defaults.js +172 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/jiti-loader.d.ts +1 -0
- package/dist/config/jiti-loader.js +13 -0
- package/dist/config/jiti-loader.js.map +1 -0
- package/dist/config/load.d.ts +8 -0
- package/dist/config/load.js +53 -0
- package/dist/config/load.js.map +1 -0
- package/dist/config/merge.d.ts +3 -0
- package/dist/config/merge.js +90 -0
- package/dist/config/merge.js.map +1 -0
- package/dist/config/schema.d.ts +32 -0
- package/dist/config/schema.js +4 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/validate.d.ts +2 -0
- package/dist/config/validate.js +128 -0
- package/dist/config/validate.js.map +1 -0
- package/dist/detect/package-json.d.ts +1 -0
- package/dist/detect/package-json.js +15 -0
- package/dist/detect/package-json.js.map +1 -0
- package/dist/detect/tool.d.ts +10 -0
- package/dist/detect/tool.js +73 -0
- package/dist/detect/tool.js.map +1 -0
- package/dist/eslint-runner.d.ts +7 -0
- package/dist/eslint-runner.js +34 -0
- package/dist/eslint-runner.js.map +1 -0
- package/dist/git/exec.d.ts +8 -0
- package/dist/git/exec.js +48 -0
- package/dist/git/exec.js.map +1 -0
- package/dist/git/resolve-scope.d.ts +15 -0
- package/dist/git/resolve-scope.js +89 -0
- package/dist/git/resolve-scope.js.map +1 -0
- package/dist/git/scope.d.ts +7 -0
- package/dist/git/scope.js +58 -0
- package/dist/git/scope.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/loader.d.ts +6 -0
- package/dist/prompts/loader.js +27 -0
- package/dist/prompts/loader.js.map +1 -0
- package/dist/prompts/packaged-dir.d.ts +1 -0
- package/dist/prompts/packaged-dir.js +10 -0
- package/dist/prompts/packaged-dir.js.map +1 -0
- package/dist/prompts/registry.d.ts +3 -0
- package/dist/prompts/registry.js +68 -0
- package/dist/prompts/registry.js.map +1 -0
- package/dist/reporter.d.ts +7 -0
- package/dist/reporter.js +114 -0
- package/dist/reporter.js.map +1 -0
- package/dist/rules/registry.d.ts +3 -0
- package/dist/rules/registry.js +37 -0
- package/dist/rules/registry.js.map +1 -0
- package/dist/runner.d.ts +15 -0
- package/dist/runner.js +151 -0
- package/dist/runner.js.map +1 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/wrap/notices.d.ts +13 -0
- package/dist/wrap/notices.js +29 -0
- package/dist/wrap/notices.js.map +1 -0
- package/dist/wrap/resolve.d.ts +5 -0
- package/dist/wrap/resolve.js +9 -0
- package/dist/wrap/resolve.js.map +1 -0
- package/dist/wrap/run.d.ts +15 -0
- package/dist/wrap/run.js +26 -0
- package/dist/wrap/run.js.map +1 -0
- package/dist/wrap/shell.d.ts +14 -0
- package/dist/wrap/shell.js +44 -0
- package/dist/wrap/shell.js.map +1 -0
- package/package.json +46 -0
- package/src/prompts/comment-non-essential.md +7 -0
- package/src/prompts/eslint-boundaries-dependencies.md +9 -0
- package/src/prompts/eslint-complexity.md +9 -0
- package/src/prompts/eslint-eqeqeq.md +3 -0
- package/src/prompts/eslint-fatal.md +7 -0
- package/src/prompts/eslint-max-lines-per-function.md +9 -0
- package/src/prompts/eslint-max-lines.md +9 -0
- package/src/prompts/eslint-max-params.md +9 -0
- package/src/prompts/eslint-no-duplicate-imports.md +1 -0
- package/src/prompts/eslint-no-unused-vars.md +3 -0
- package/src/prompts/eslint-no-var.md +1 -0
- package/src/prompts/eslint-no-warning-comments.md +3 -0
- package/src/prompts/eslint-prefer-const.md +3 -0
- package/src/prompts/eslint-typescript-eslint-no-explicit-any.md +3 -0
- package/src/prompts/eslint-typescript-eslint-no-inferrable-types.md +3 -0
- package/src/prompts/eslint-typescript-eslint-no-non-null-assertion.md +3 -0
- package/src/prompts/jscpd-duplication.md +9 -0
- package/src/prompts/knip-classMembers.md +7 -0
- package/src/prompts/knip-dependencies.md +9 -0
- package/src/prompts/knip-exports.md +9 -0
- package/src/prompts/knip-files.md +9 -0
- package/src/prompts/knip-types.md +9 -0
- package/src/prompts/uncoached.md +3 -0
- package/src/skills/habit-hooks-review/SKILL.md +108 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ivett Ördög
|
|
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
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# habit-hooks
|
|
2
|
+
|
|
3
|
+
Stop reciting software engineering literature to your AI agent.
|
|
4
|
+
|
|
5
|
+
Turn best practice advice into AI habits.
|
|
6
|
+
|
|
7
|
+
## What it is
|
|
8
|
+
|
|
9
|
+
AI coding agents frequently ignore long rule documents. Asking them to hold on to an entire book's worth of
|
|
10
|
+
coding advice is at best futile, at worst makes the agent's performance worse by polluting the context window.
|
|
11
|
+
|
|
12
|
+
Humans don't need to hold the same information in their head because humans can form habits through repetition.
|
|
13
|
+
However, AI agents can't do this.
|
|
14
|
+
|
|
15
|
+
Human habits form when an easy-to-detect cue triggers a complex sequence of actions with the desired effect.
|
|
16
|
+
This is the inspiration for habit hooks.
|
|
17
|
+
|
|
18
|
+
Linters provide a deterministic metric, but Goodhart's law postulates that a metric ceases to be a good metric if
|
|
19
|
+
it becomes a target. AI agents are very good at gaming these metrics when they are only provided the metric.
|
|
20
|
+
|
|
21
|
+
Habit hooks wraps your linter to create the trigger, but instead of providing only the metric, it gives actionable
|
|
22
|
+
advice on how to fix the issue. This creates AI behaviour that looks like human habits, and has similar effects.
|
|
23
|
+
|
|
24
|
+
The use of habit hooks:
|
|
25
|
+
- Increases code quality
|
|
26
|
+
- Improves AI performance ensuring that the AI always starts with good code quality
|
|
27
|
+
- Reduces token usage, since good quality code also means the AI doesn't need to read as much context to complete the task.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npm install --save-dev habit-hooks
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
habit-hooks depends on `eslint`, `knip`, and `jscpd`, so installing it pulls those in as well — a fresh project gets every wrap target for free. If your project already has its own versions installed, habit-hooks detects and uses those instead and falls back to the bundled binaries only when none is present.
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npx habit-hooks init
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`init` detects which of eslint / knip / jscpd are already installed and configured, scaffolds starter configs for the missing ones, writes `habit-hooks.config.js` and an empty baseline, and offers to wire up `package.json` scripts, a pre-commit hook, and the bundled reviewer skill. Run with `--dry-run` to see every intended write without touching disk.
|
|
44
|
+
|
|
45
|
+
Then:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
npx habit-hooks
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
That runs every wrapped tool against files changed since the branch base.
|
|
52
|
+
|
|
53
|
+
## What it catches
|
|
54
|
+
|
|
55
|
+
Habit-hooks wraps your existing eslint, knip, and jscpd. Whatever rules and thresholds those tools fire is what habit-hooks surfaces — the rules come from your project's `eslint.config.*`, `knip.json`, `.jscpd.json` (or matching `package.json` keys), not from habit-hooks.
|
|
56
|
+
|
|
57
|
+
What habit-hooks adds on top is the *why this is a smell* and *how to fix it* guidance. For the rule ids below we ship a coaching prompt; everything else surfaces under a single "Uncoached rules" section so you don't lose visibility on rules we haven't tuned.
|
|
58
|
+
|
|
59
|
+
**Coached rule ids**
|
|
60
|
+
|
|
61
|
+
| Source | Rule id |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| eslint | `eslint:max-lines-per-function` |
|
|
64
|
+
| eslint | `eslint:max-params` |
|
|
65
|
+
| eslint | `eslint:complexity` |
|
|
66
|
+
| eslint | `eslint:max-lines` |
|
|
67
|
+
| eslint | `eslint:no-unused-vars` |
|
|
68
|
+
| eslint | `eslint:eqeqeq` |
|
|
69
|
+
| eslint | `eslint:no-var` |
|
|
70
|
+
| eslint | `eslint:prefer-const` |
|
|
71
|
+
| eslint | `eslint:no-duplicate-imports` |
|
|
72
|
+
| eslint | `eslint:no-warning-comments` |
|
|
73
|
+
| eslint | `eslint:@typescript-eslint/no-explicit-any` |
|
|
74
|
+
| eslint | `eslint:@typescript-eslint/no-non-null-assertion` |
|
|
75
|
+
| eslint | `eslint:@typescript-eslint/no-inferrable-types` |
|
|
76
|
+
| eslint | `eslint:boundaries/dependencies` |
|
|
77
|
+
| knip | `knip:classMembers` |
|
|
78
|
+
| knip | `knip:files` |
|
|
79
|
+
| knip | `knip:exports` |
|
|
80
|
+
| knip | `knip:types` |
|
|
81
|
+
| knip | `knip:dependencies` |
|
|
82
|
+
| jscpd | `jscpd:duplication` |
|
|
83
|
+
| custom | `comment:non-essential` |
|
|
84
|
+
|
|
85
|
+
**Uncoached rules**
|
|
86
|
+
|
|
87
|
+
Any rule habit-hooks doesn't yet coach still gets surfaced — grouped under a single "Uncoached rules" section in the output so the agent can see what fired. To add coaching for a rule, drop a `<slugified-rule-id>.md` file in the configured prompts directory (replace `:` and `/` with `-`, drop `@`). habit-hooks will use that prompt instead of treating the rule as uncoached.
|
|
88
|
+
|
|
89
|
+
## CLI
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
habit-hooks run all wrapped checks against the default scope
|
|
93
|
+
habit-hooks --last <n> check files changed in the last N commits
|
|
94
|
+
habit-hooks --branch [name] check files changed vs branch (default: scope.branchBase)
|
|
95
|
+
habit-hooks --since <hash> check files changed since the given commit
|
|
96
|
+
habit-hooks --all force checking all files (ignore scope config)
|
|
97
|
+
habit-hooks --config <path> use an explicit config file
|
|
98
|
+
habit-hooks --version print version
|
|
99
|
+
|
|
100
|
+
habit-hooks init scaffold tool configs, habit-hooks config, scripts, hooks
|
|
101
|
+
habit-hooks init --dry-run show every intended write without touching disk
|
|
102
|
+
|
|
103
|
+
habit-hooks baseline generate write a fresh baseline snapshot
|
|
104
|
+
habit-hooks baseline status summarise current baseline contents
|
|
105
|
+
habit-hooks baseline snooze add the current violations to the baseline
|
|
106
|
+
habit-hooks baseline forget remove specific files from the baseline
|
|
107
|
+
habit-hooks baseline prune drop baseline entries whose files no longer exist
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`--last`, `--branch`, `--since`, and `--all` are mutually exclusive.
|
|
111
|
+
|
|
112
|
+
## Opinionated by design
|
|
113
|
+
|
|
114
|
+
habit-hooks ships with strong opinions baked in: small functions, few parameters, low complexity, no comments standing in for unclear code, no `any`, no dead exports, no copy-pasted blocks. The scaffolded ESLint config from `npx habit-hooks init` reflects those opinions (12-line functions, 3-param max, etc.).
|
|
115
|
+
|
|
116
|
+
If you disagree with a threshold, change it. Every rule habit-hooks coaches comes from your project's own `eslint.config.*` / `knip.json` / `.jscpd.json` — you have full control. The bundled coaching prompts assume the opinionated defaults; if you loosen a threshold significantly the prompt may read a bit overconfident, but it will still point in the right direction.
|
|
117
|
+
|
|
118
|
+
## Configuration
|
|
119
|
+
|
|
120
|
+
habit-hooks looks for `habit-hooks.config.ts` (or `.js` / `.mjs`) in the project root. The config shape is intentionally small — all rule thresholds, plugin choices, and ignores live in your eslint / knip / jscpd configs, not here.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
// habit-hooks.config.ts
|
|
124
|
+
import type { HabitHooksConfig } from 'habit-hooks';
|
|
125
|
+
|
|
126
|
+
const config: HabitHooksConfig = {
|
|
127
|
+
prompts: './prompts',
|
|
128
|
+
rules: {
|
|
129
|
+
'comment:non-essential': { disabled: true },
|
|
130
|
+
'eslint:max-params': { exclude: ['**/*.test.ts', 'tests/**'] },
|
|
131
|
+
},
|
|
132
|
+
scope: {
|
|
133
|
+
onlyChangedFiles: true,
|
|
134
|
+
branchBase: 'main',
|
|
135
|
+
},
|
|
136
|
+
commentCheck: {
|
|
137
|
+
maxSingleLineChars: 10,
|
|
138
|
+
maxBlockChars: 15,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export default config;
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
What you can set per rule: `disabled`, `include`, `exclude`, `severity`. Everything else (e.g. `max-params: ['error', { max: 5 }]`) belongs in `eslint.config.*`. The `prompts` directory lets you override or add coaching text — drop a `<rule-id>.md` file in there (with `:` and `/` replaced by `-`, `@` dropped) and habit-hooks will use it instead of the bundled prompt. The `commentCheck` block tunes the character thresholds at which the custom `comment:non-essential` rule starts flagging single-line and block comments (defaults shown above). See `src/config/schema.ts` for the full schema.
|
|
146
|
+
|
|
147
|
+
Note: `disabled: true` only suppresses the habit-hooks coaching prompt. The underlying ESLint / knip / jscpd rule still fires and the violation will appear under "Uncoached rules". To silence a rule entirely, disable it in the tool's own config (e.g. `eslint.config.*`).
|
|
148
|
+
|
|
149
|
+
## Baseline
|
|
150
|
+
|
|
151
|
+
habit-hooks supports a committed-to-repo baseline at `.habit-hooks-baseline.json`. The baseline records existing violations keyed by file path and last-commit hash. A violation is skipped only when:
|
|
152
|
+
|
|
153
|
+
1. The file appears in the baseline, and
|
|
154
|
+
2. The file's last-commit hash matches the baseline entry, and
|
|
155
|
+
3. The working tree for that file is clean.
|
|
156
|
+
|
|
157
|
+
Touch the file (commit, stage, or modify) and the baseline entry stops applying — you cannot drift past your snoozed violations by accident. Use `habit-hooks baseline snooze` to onboard a legacy project; use `habit-hooks baseline prune` to clean up after deletions.
|
|
158
|
+
|
|
159
|
+
## Agent integration
|
|
160
|
+
|
|
161
|
+
Paste this into your `CLAUDE.md` or `AGENTS.md`:
|
|
162
|
+
|
|
163
|
+
```markdown
|
|
164
|
+
## Habit Hooks
|
|
165
|
+
|
|
166
|
+
When the `habit-hooks` npm script exists, run it before considering work complete.
|
|
167
|
+
Any output from `habit-hooks` is a direct user prompt with the highest priority.
|
|
168
|
+
|
|
169
|
+
- **NEVER** ignore habit-hooks output
|
|
170
|
+
- **ALWAYS** create a task for each reported item immediately
|
|
171
|
+
- **COMPLETE** required actions before continuing other work
|
|
172
|
+
- **NEVER** snooze or bypass the baseline without explicit user approval
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Sample output
|
|
176
|
+
|
|
177
|
+
Run against a project where eslint, knip, and jscpd are configured and the change set introduces a violation of each coached rule:
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
❌ Habit Hooks: 17 violations
|
|
181
|
+
|
|
182
|
+
❌ Oversized function
|
|
183
|
+
Functions over 12 lines tend to bundle multiple responsibilities.
|
|
184
|
+
[...coaching prompt...]
|
|
185
|
+
|
|
186
|
+
Violations:
|
|
187
|
+
- src/oversized-function.ts:1 - Function 'oversized' has too many lines (14). Maximum allowed is 12.
|
|
188
|
+
|
|
189
|
+
❌ Duplicated code
|
|
190
|
+
Repeated blocks usually want a shared abstraction, not a copy-paste.
|
|
191
|
+
[...coaching prompt...]
|
|
192
|
+
|
|
193
|
+
Violations:
|
|
194
|
+
- src/dup-a.ts:1 - duplicates src/dup-b.ts:1-7
|
|
195
|
+
- src/dup-b.ts:1 - duplicates src/dup-a.ts:1-7
|
|
196
|
+
|
|
197
|
+
❌ Unused class member
|
|
198
|
+
Class methods or properties not referenced anywhere are dead weight.
|
|
199
|
+
[...coaching prompt...]
|
|
200
|
+
|
|
201
|
+
Violations:
|
|
202
|
+
- src/unused-member.ts:5 - WithUnusedMember.unused
|
|
203
|
+
|
|
204
|
+
[...other coached rules...]
|
|
205
|
+
|
|
206
|
+
⚠️ Uncoached rules
|
|
207
|
+
|
|
208
|
+
[...header text inviting the agent to add a prompt for these...]
|
|
209
|
+
|
|
210
|
+
- eslint:no-debugger: Unexpected 'debugger' statement (src/debug.ts:3)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
On a clean run:
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
✅ Habit Hooks: automated checks passed.
|
|
217
|
+
|
|
218
|
+
Habit Hooks catches structural smells, not correctness or design. If no reviewer sub-agent has reviewed this change set, run one before declaring done.
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
That closing message is the cue for the `habit-hooks-review` skill — see `src/skills/habit-hooks-review/SKILL.md`.
|
|
222
|
+
|
|
223
|
+
## Status
|
|
224
|
+
|
|
225
|
+
v2 wrap pivot landed: eslint / knip / jscpd are now wrapped (not invoked programmatically), the rule set comes from your project configs, and `init` does the heavy lifting of scaffolding starter configs. Pre-release; the first npm publish is pending.
|
|
226
|
+
|
|
227
|
+
## Contributing
|
|
228
|
+
|
|
229
|
+
PRs are welcome! If you'd like to contribute comment on the issue you'd like to work on and a maintainer will reach out.
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
MIT — see [`LICENSE.md`](./LICENSE.md).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type BaselineFile } from './store.js';
|
|
2
|
+
export interface CommandResult {
|
|
3
|
+
stdout: string;
|
|
4
|
+
stderr: string;
|
|
5
|
+
exitCode: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function baselineGenerate(cwd: string): Promise<CommandResult>;
|
|
8
|
+
export declare function baselineSnooze(cwd: string, paths: string[]): CommandResult;
|
|
9
|
+
export declare function baselineForget(cwd: string, paths: string[]): CommandResult;
|
|
10
|
+
export declare function baselinePrune(cwd: string): Promise<CommandResult>;
|
|
11
|
+
export declare function baselineStatus(cwd: string): CommandResult;
|
|
12
|
+
export type { BaselineFile };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { run } from '../runner.js';
|
|
4
|
+
import { isWorkingTreeCleanFor, lastCommitHash } from './file-hash.js';
|
|
5
|
+
import { BASELINE_VERSION, baselineExists, loadBaseline, saveBaseline, } from './store.js';
|
|
6
|
+
import { toRepoRelative } from './filter.js';
|
|
7
|
+
function ok(stdout = '') {
|
|
8
|
+
return { stdout, stderr: '', exitCode: 0 };
|
|
9
|
+
}
|
|
10
|
+
function err(stderr, exitCode = 2) {
|
|
11
|
+
return { stdout: '', stderr, exitCode };
|
|
12
|
+
}
|
|
13
|
+
function uniqueRelPaths(cwd, paths) {
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
for (const p of paths)
|
|
16
|
+
seen.add(toRepoRelative(cwd, join(cwd, p)));
|
|
17
|
+
return [...seen];
|
|
18
|
+
}
|
|
19
|
+
async function collectViolatingFiles(cwd) {
|
|
20
|
+
const result = await run(cwd, {
|
|
21
|
+
scopeFlags: { all: true },
|
|
22
|
+
applyBaseline: false,
|
|
23
|
+
});
|
|
24
|
+
const out = new Set();
|
|
25
|
+
for (const violation of result.violations) {
|
|
26
|
+
out.add(toRepoRelative(cwd, violation.file));
|
|
27
|
+
}
|
|
28
|
+
return [...out];
|
|
29
|
+
}
|
|
30
|
+
function tryAddSnoozeEntry(files, cwd, relPath) {
|
|
31
|
+
const hash = lastCommitHash(cwd, relPath);
|
|
32
|
+
if (hash === null)
|
|
33
|
+
return false;
|
|
34
|
+
files[relPath] = { snoozedAtCommit: hash };
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
export async function baselineGenerate(cwd) {
|
|
38
|
+
const violatingFiles = await collectViolatingFiles(cwd);
|
|
39
|
+
const files = { ...loadBaseline(cwd).files };
|
|
40
|
+
const added = violatingFiles.filter((p) => tryAddSnoozeEntry(files, cwd, p)).length;
|
|
41
|
+
saveBaseline(cwd, { version: BASELINE_VERSION, files });
|
|
42
|
+
return ok(`baseline generate: recorded ${String(added)} file(s)\n`);
|
|
43
|
+
}
|
|
44
|
+
function buildSnoozeBatch(cwd, targets) {
|
|
45
|
+
const errors = [];
|
|
46
|
+
const updates = {};
|
|
47
|
+
for (const rel of targets) {
|
|
48
|
+
const validation = validateSnoozeTarget(cwd, rel);
|
|
49
|
+
if (validation.error !== null)
|
|
50
|
+
errors.push(validation.error);
|
|
51
|
+
else
|
|
52
|
+
updates[rel] = { snoozedAtCommit: validation.hash };
|
|
53
|
+
}
|
|
54
|
+
return { errors, updates };
|
|
55
|
+
}
|
|
56
|
+
export function baselineSnooze(cwd, paths) {
|
|
57
|
+
const baseline = loadBaseline(cwd);
|
|
58
|
+
const targets = uniqueRelPaths(cwd, paths);
|
|
59
|
+
const { errors, updates } = buildSnoozeBatch(cwd, targets);
|
|
60
|
+
if (errors.length > 0)
|
|
61
|
+
return err(`${errors.join('\n')}\n`);
|
|
62
|
+
saveBaseline(cwd, { version: BASELINE_VERSION, files: { ...baseline.files, ...updates } });
|
|
63
|
+
return ok(`baseline snooze: added ${String(Object.keys(updates).length)} file(s)\n`);
|
|
64
|
+
}
|
|
65
|
+
function snoozeRejection(relPath, reason) {
|
|
66
|
+
return { error: `cannot snooze '${relPath}': ${reason}`, hash: '' };
|
|
67
|
+
}
|
|
68
|
+
function validateSnoozeTarget(cwd, relPath) {
|
|
69
|
+
if (!existsSync(join(cwd, relPath)))
|
|
70
|
+
return snoozeRejection(relPath, 'file does not exist');
|
|
71
|
+
const hash = lastCommitHash(cwd, relPath);
|
|
72
|
+
if (hash === null)
|
|
73
|
+
return snoozeRejection(relPath, 'file is untracked (commit it first)');
|
|
74
|
+
return { error: null, hash };
|
|
75
|
+
}
|
|
76
|
+
function deleteEntry(files, key) {
|
|
77
|
+
if (!(key in files))
|
|
78
|
+
return false;
|
|
79
|
+
delete files[key];
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
export function baselineForget(cwd, paths) {
|
|
83
|
+
const files = { ...loadBaseline(cwd).files };
|
|
84
|
+
const targets = uniqueRelPaths(cwd, paths);
|
|
85
|
+
const removed = targets.filter((rel) => deleteEntry(files, rel)).length;
|
|
86
|
+
if (removed > 0)
|
|
87
|
+
saveBaseline(cwd, { version: BASELINE_VERSION, files });
|
|
88
|
+
return ok(`baseline forget: removed ${String(removed)} entry/entries\n`);
|
|
89
|
+
}
|
|
90
|
+
function shouldKeepEntry(cwd, relPath, violating) {
|
|
91
|
+
if (!existsSync(join(cwd, relPath)))
|
|
92
|
+
return false;
|
|
93
|
+
return violating.has(relPath);
|
|
94
|
+
}
|
|
95
|
+
export async function baselinePrune(cwd) {
|
|
96
|
+
const baseline = loadBaseline(cwd);
|
|
97
|
+
const violating = new Set(await collectViolatingFiles(cwd));
|
|
98
|
+
const kept = Object.entries(baseline.files).filter(([rel]) => shouldKeepEntry(cwd, rel, violating));
|
|
99
|
+
const files = Object.fromEntries(kept);
|
|
100
|
+
const removed = Object.keys(baseline.files).length - kept.length;
|
|
101
|
+
saveBaseline(cwd, { version: BASELINE_VERSION, files });
|
|
102
|
+
return ok(`baseline prune: removed ${String(removed)} entry/entries\n`);
|
|
103
|
+
}
|
|
104
|
+
function classifyEntry(cwd, relPath, entry) {
|
|
105
|
+
if (!existsSync(join(cwd, relPath)))
|
|
106
|
+
return 'stale-missing';
|
|
107
|
+
const hash = lastCommitHash(cwd, relPath);
|
|
108
|
+
if (hash === null)
|
|
109
|
+
return 'stale-missing';
|
|
110
|
+
if (hash !== entry.snoozedAtCommit)
|
|
111
|
+
return 'stale-changed';
|
|
112
|
+
if (!isWorkingTreeCleanFor(cwd, relPath))
|
|
113
|
+
return 'stale-changed';
|
|
114
|
+
return 'current';
|
|
115
|
+
}
|
|
116
|
+
function renderStatusLines(cwd, entries) {
|
|
117
|
+
const lines = [`Baseline: ${String(entries.length)} snoozed file(s)`];
|
|
118
|
+
for (const [rel, entry] of entries) {
|
|
119
|
+
lines.push(` [${classifyEntry(cwd, rel, entry)}] ${rel}`);
|
|
120
|
+
}
|
|
121
|
+
return lines;
|
|
122
|
+
}
|
|
123
|
+
export function baselineStatus(cwd) {
|
|
124
|
+
if (!baselineExists(cwd))
|
|
125
|
+
return ok('No baseline file found.\n');
|
|
126
|
+
const entries = Object.entries(loadBaseline(cwd).files).sort(([a], [b]) => a.localeCompare(b));
|
|
127
|
+
if (entries.length === 0)
|
|
128
|
+
return ok('Baseline file is empty.\n');
|
|
129
|
+
return ok(`${renderStatusLines(cwd, entries).join('\n')}\n`);
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=commands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../../src/baseline/commands.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { gitExec, GitError } from '../git/exec.js';
|
|
2
|
+
export function lastCommitHash(cwd, relPath) {
|
|
3
|
+
try {
|
|
4
|
+
const out = gitExec(['log', '-n', '1', '--format=%H', '--', relPath], cwd);
|
|
5
|
+
const hash = out.trim();
|
|
6
|
+
return hash.length === 0 ? null : hash;
|
|
7
|
+
}
|
|
8
|
+
catch (err) {
|
|
9
|
+
if (err instanceof GitError)
|
|
10
|
+
return null;
|
|
11
|
+
throw err;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function isWorkingTreeCleanFor(cwd, relPath) {
|
|
15
|
+
try {
|
|
16
|
+
const out = gitExec(['status', '--porcelain', '--', relPath], cwd);
|
|
17
|
+
return out.trim().length === 0;
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
if (err instanceof GitError)
|
|
21
|
+
return false;
|
|
22
|
+
throw err;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=file-hash.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-hash.js","sourceRoot":"","sources":["../../src/baseline/file-hash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAEnD,MAAM,UAAU,cAAc,CAAC,GAAW,EAAE,OAAe;IACzD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,GAAG,CAAC,CAAC;QAC3E,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QACxB,OAAO,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IACzC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,QAAQ;YAAE,OAAO,IAAI,CAAC;QACzC,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,GAAW,EAAE,OAAe;IAChE,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,QAAQ,EAAE,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,GAAG,CAAC,CAAC;QACnE,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,CAAC;IACjC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,QAAQ;YAAE,OAAO,KAAK,CAAC;QAC1C,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BaselineFile } from './store.js';
|
|
2
|
+
export declare function toRepoRelative(cwd: string, absPath: string): string;
|
|
3
|
+
interface SnoozePartition {
|
|
4
|
+
active: string[];
|
|
5
|
+
skipped: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function partitionBySnooze(files: string[], baseline: BaselineFile, cwd: string): SnoozePartition;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { relative, sep } from 'node:path';
|
|
2
|
+
import { isWorkingTreeCleanFor, lastCommitHash } from './file-hash.js';
|
|
3
|
+
export function toRepoRelative(cwd, absPath) {
|
|
4
|
+
return relative(cwd, absPath).split(sep).join('/');
|
|
5
|
+
}
|
|
6
|
+
function isSnoozed(relPath, baseline, cwd) {
|
|
7
|
+
const entry = baseline.files[relPath];
|
|
8
|
+
if (entry === undefined)
|
|
9
|
+
return false;
|
|
10
|
+
const currentHash = lastCommitHash(cwd, relPath);
|
|
11
|
+
if (currentHash === null)
|
|
12
|
+
return false;
|
|
13
|
+
if (currentHash !== entry.snoozedAtCommit)
|
|
14
|
+
return false;
|
|
15
|
+
return isWorkingTreeCleanFor(cwd, relPath);
|
|
16
|
+
}
|
|
17
|
+
function assignToPartition(partition, abs, snoozed) {
|
|
18
|
+
if (snoozed)
|
|
19
|
+
partition.skipped.push(abs);
|
|
20
|
+
else
|
|
21
|
+
partition.active.push(abs);
|
|
22
|
+
}
|
|
23
|
+
export function partitionBySnooze(files, baseline, cwd) {
|
|
24
|
+
const partition = { active: [], skipped: [] };
|
|
25
|
+
for (const abs of files) {
|
|
26
|
+
const rel = toRepoRelative(cwd, abs);
|
|
27
|
+
assignToPartition(partition, abs, isSnoozed(rel, baseline, cwd));
|
|
28
|
+
}
|
|
29
|
+
return partition;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter.js","sourceRoot":"","sources":["../../src/baseline/filter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAGvE,MAAM,UAAU,cAAc,CAAC,GAAW,EAAE,OAAe;IACzD,OAAO,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACrD,CAAC;AAED,SAAS,SAAS,CAAC,OAAe,EAAE,QAAsB,EAAE,GAAW;IACrE,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACtC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACtC,MAAM,WAAW,GAAG,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACjD,IAAI,WAAW,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IACvC,IAAI,WAAW,KAAK,KAAK,CAAC,eAAe;QAAE,OAAO,KAAK,CAAC;IACxD,OAAO,qBAAqB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AAC7C,CAAC;AAOD,SAAS,iBAAiB,CACxB,SAA0B,EAC1B,GAAW,EACX,OAAgB;IAEhB,IAAI,OAAO;QAAE,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;;QACpC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,KAAe,EACf,QAAsB,EACtB,GAAW;IAEX,MAAM,SAAS,GAAoB,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IAC/D,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,cAAc,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACrC,iBAAiB,CAAC,SAAS,EAAE,GAAG,EAAE,SAAS,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC;IACnE,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const BASELINE_FILENAME = ".habit-hooks-baseline.json";
|
|
2
|
+
export declare const BASELINE_VERSION = 2;
|
|
3
|
+
export interface BaselineEntry {
|
|
4
|
+
snoozedAtCommit: string;
|
|
5
|
+
}
|
|
6
|
+
export interface BaselineFile {
|
|
7
|
+
version: 2;
|
|
8
|
+
files: Record<string, BaselineEntry>;
|
|
9
|
+
}
|
|
10
|
+
export declare class BaselineVersionError extends Error {
|
|
11
|
+
constructor(version: number);
|
|
12
|
+
}
|
|
13
|
+
export declare class BaselineParseError extends Error {
|
|
14
|
+
constructor(message: string);
|
|
15
|
+
}
|
|
16
|
+
export declare function loadBaseline(cwd: string): BaselineFile;
|
|
17
|
+
export declare function saveBaseline(cwd: string, baseline: BaselineFile): void;
|
|
18
|
+
export declare function baselineExists(cwd: string): boolean;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export const BASELINE_FILENAME = '.habit-hooks-baseline.json';
|
|
4
|
+
export const BASELINE_VERSION = 2;
|
|
5
|
+
const LEGACY_BASELINE_VERSION = 1;
|
|
6
|
+
export class BaselineVersionError extends Error {
|
|
7
|
+
constructor(version) {
|
|
8
|
+
super(`unsupported baseline version ${String(version)}; expected 1 or 2`);
|
|
9
|
+
this.name = 'BaselineVersionError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class BaselineParseError extends Error {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(`failed to parse baseline: ${message}`);
|
|
15
|
+
this.name = 'BaselineParseError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function baselinePath(cwd) {
|
|
19
|
+
return join(cwd, BASELINE_FILENAME);
|
|
20
|
+
}
|
|
21
|
+
function emptyBaseline() {
|
|
22
|
+
return { version: BASELINE_VERSION, files: {} };
|
|
23
|
+
}
|
|
24
|
+
function parseRaw(raw) {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(raw);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30
|
+
throw new BaselineParseError(message);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function isPlainObject(value) {
|
|
34
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
function validateHashField(value, key, field) {
|
|
37
|
+
const hash = value[field];
|
|
38
|
+
if (typeof hash !== 'string' || hash.length === 0) {
|
|
39
|
+
throw new BaselineParseError(`entry for '${key}' is missing '${field}' string`);
|
|
40
|
+
}
|
|
41
|
+
return hash;
|
|
42
|
+
}
|
|
43
|
+
function validateEntry(value, key, field) {
|
|
44
|
+
if (!isPlainObject(value)) {
|
|
45
|
+
throw new BaselineParseError(`entry for '${key}' must be an object`);
|
|
46
|
+
}
|
|
47
|
+
return { snoozedAtCommit: validateHashField(value, key, field) };
|
|
48
|
+
}
|
|
49
|
+
function mapEntries(value, field) {
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
52
|
+
out[key] = validateEntry(entry, key, field);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
function validateFiles(value, field) {
|
|
57
|
+
if (!isPlainObject(value))
|
|
58
|
+
throw new BaselineParseError(`'files' must be an object`);
|
|
59
|
+
return mapEntries(value, field);
|
|
60
|
+
}
|
|
61
|
+
function validateVersion(value) {
|
|
62
|
+
if (typeof value !== 'number') {
|
|
63
|
+
throw new BaselineParseError(`'version' must be a number`);
|
|
64
|
+
}
|
|
65
|
+
if (value !== BASELINE_VERSION && value !== LEGACY_BASELINE_VERSION) {
|
|
66
|
+
throw new BaselineVersionError(value);
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
function fieldForVersion(version) {
|
|
71
|
+
return version === LEGACY_BASELINE_VERSION ? 'snoozedAt' : 'snoozedAtCommit';
|
|
72
|
+
}
|
|
73
|
+
function validateBaseline(value) {
|
|
74
|
+
if (!isPlainObject(value)) {
|
|
75
|
+
throw new BaselineParseError(`root must be an object`);
|
|
76
|
+
}
|
|
77
|
+
const version = validateVersion(value.version);
|
|
78
|
+
const files = validateFiles(value.files, fieldForVersion(version));
|
|
79
|
+
return { version: BASELINE_VERSION, files };
|
|
80
|
+
}
|
|
81
|
+
export function loadBaseline(cwd) {
|
|
82
|
+
const path = baselinePath(cwd);
|
|
83
|
+
if (!existsSync(path))
|
|
84
|
+
return emptyBaseline();
|
|
85
|
+
const raw = readFileSync(path, 'utf8');
|
|
86
|
+
return validateBaseline(parseRaw(raw));
|
|
87
|
+
}
|
|
88
|
+
function sortedEntries(files) {
|
|
89
|
+
const sorted = {};
|
|
90
|
+
for (const key of Object.keys(files).sort()) {
|
|
91
|
+
sorted[key] = files[key];
|
|
92
|
+
}
|
|
93
|
+
return sorted;
|
|
94
|
+
}
|
|
95
|
+
export function saveBaseline(cwd, baseline) {
|
|
96
|
+
const ordered = {
|
|
97
|
+
version: BASELINE_VERSION,
|
|
98
|
+
files: sortedEntries(baseline.files),
|
|
99
|
+
};
|
|
100
|
+
const serialized = `${JSON.stringify(ordered, null, 2)}\n`;
|
|
101
|
+
writeFileSync(baselinePath(cwd), serialized);
|
|
102
|
+
}
|
|
103
|
+
export function baselineExists(cwd) {
|
|
104
|
+
return existsSync(baselinePath(cwd));
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/baseline/store.ts"],"names":[],"mappings":"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"}
|