@savvy-web/cli 3.5.1 → 3.6.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/cli/index.js +19 -3
- package/commands/changeset/commands/check.js +82 -21
- package/commands/changeset/commands/config-validate.js +9 -6
- package/commands/changeset/commands/deps-detect.js +2 -2
- package/commands/changeset/commands/deps-regen.js +94 -48
- package/commands/changeset/commands/init.js +56 -23
- package/commands/changeset/commands/transform.js +82 -8
- package/commands/changeset/commands/validate-file.js +12 -11
- package/commands/changeset/commands/version.js +48 -17
- package/commands/changeset/index.js +3 -3
- package/commands/changeset/utils/config-gate.js +12 -10
- package/commands/check-section.js +34 -0
- package/commands/check.js +48 -39
- package/commands/clean/progress.js +71 -0
- package/commands/clean/view.js +26 -0
- package/commands/clean.js +113 -50
- package/commands/commit/check.js +80 -77
- package/commands/commit/init.js +21 -10
- package/commands/commit/lint.js +2 -2
- package/commands/init.js +122 -34
- package/commands/lint/check.js +126 -171
- package/commands/lint/init.js +52 -33
- package/commands/lint/text.js +28 -13
- package/commands/repos/commands/add.js +14 -17
- package/commands/repos/commands/deregister.js +37 -18
- package/commands/repos/commands/note.js +78 -26
- package/commands/repos/commands/pin.js +39 -24
- package/commands/repos/commands/remove.js +30 -56
- package/commands/repos/commands/rename.js +32 -23
- package/commands/repos/commands/restore.js +42 -56
- package/commands/repos/commands/status.js +43 -47
- package/commands/repos/commands/sync.js +11 -40
- package/commands/repos/index.js +7 -10
- package/commands/repos/shared.js +129 -0
- package/index.d.ts +162 -61
- package/internal/command-error.js +60 -0
- package/internal/confirm.js +52 -0
- package/internal/failure-line.js +26 -12
- package/internal/report.js +101 -0
- package/main.js +23 -16
- package/package.json +17 -12
- package/version.js +1 -1
- package/internal/output.js +0 -52
package/commands/init.js
CHANGED
|
@@ -1,38 +1,118 @@
|
|
|
1
|
+
import { Report } from "../internal/report.js";
|
|
2
|
+
import { CommandError } from "../internal/command-error.js";
|
|
1
3
|
import { runChangesetInit } from "./changeset/commands/init.js";
|
|
2
4
|
import "./changeset/index.js";
|
|
5
|
+
import { HUSKY_HOOK_PATH } from "./commit/constants.js";
|
|
3
6
|
import { runCommitInit } from "./commit/init.js";
|
|
7
|
+
import { confirmDestructive, yesFlag } from "../internal/confirm.js";
|
|
4
8
|
import { runLintInit } from "./lint/init.js";
|
|
5
9
|
import { Command, Flag } from "effect/cli";
|
|
6
|
-
import {
|
|
10
|
+
import { Lint } from "@savvy-web/silk-effects";
|
|
11
|
+
import { Effect, FileSystem } from "effect";
|
|
12
|
+
import { CliUi, Select } from "@effected/cli/ui";
|
|
7
13
|
|
|
8
14
|
//#region src/commands/init.ts
|
|
9
|
-
/**
|
|
10
|
-
* Unified `savvy init` orchestrator.
|
|
11
|
-
*
|
|
12
|
-
* @remarks
|
|
13
|
-
* Sequences the three tool-specific init handlers — changeset → commit → lint —
|
|
14
|
-
* in a single pass. Short-circuits on the first failure. The three step Effects
|
|
15
|
-
* are injected so the orchestration logic is unit-testable in isolation.
|
|
16
|
-
*
|
|
17
|
-
* Runtime layer provision (ManagedSection, FileSystem, WorkspaceRoot,
|
|
18
|
-
* BiomeSchemaSync, etc.) is deferred to Task B7 (root `main`). The Effect
|
|
19
|
-
* returned by `initCommand`'s handler therefore carries the full union of the
|
|
20
|
-
* three handlers' requirements in its R channel.
|
|
21
|
-
*
|
|
22
|
-
* @internal
|
|
23
|
-
*/
|
|
24
15
|
const DEFAULT_COMMIT_CONFIG = "lib/configs/commitlint.config.ts";
|
|
25
16
|
const DEFAULT_LINT_CONFIG = "lib/configs/lint-staged.config.ts";
|
|
26
17
|
/* v8 ignore start -- CLI option definitions; orchestration logic tested via runInit */
|
|
27
18
|
const forceOption = Flag.Boolean("force").pipe(Flag.withAlias("f"), Flag.withDescription("Overwrite existing config files and hooks across all tools"), Flag.withDefault(false));
|
|
28
19
|
const commitConfigOption = Flag.String("commit-config").pipe(Flag.withDescription("Relative path for the commitlint config file"), Flag.withDefault(DEFAULT_COMMIT_CONFIG));
|
|
29
20
|
const lintConfigOption = Flag.String("lint-config").pipe(Flag.withDescription("Relative path for the lint-staged config file"), Flag.withDefault(DEFAULT_LINT_CONFIG));
|
|
21
|
+
/** The preset a run that cannot ask uses: an agent, CI, a pipe. */
|
|
22
|
+
const DEFAULT_LINT_PRESET = "silk";
|
|
23
|
+
/**
|
|
24
|
+
* `--lint-preset`: given, it is used as is; omitted, a person at a terminal
|
|
25
|
+
* picks one from a `Select` (starting on `silk`), and every other run uses
|
|
26
|
+
* `silk` without loading Ink or React.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
30
|
const lintPresetOption = Flag.Literals("lint-preset", [
|
|
31
31
|
"minimal",
|
|
32
32
|
"standard",
|
|
33
33
|
"silk"
|
|
34
|
-
]).pipe(Flag.withDescription("lint-staged preset: minimal, standard, or silk"), Flag.
|
|
35
|
-
|
|
34
|
+
]).pipe(Flag.withDescription("lint-staged preset: minimal, standard, or silk (asked when omitted at a terminal; default silk)"), Flag.withFallbackPrompt(CliUi.fallback(Select.screen({
|
|
35
|
+
message: "Which lint-staged preset?",
|
|
36
|
+
choices: [
|
|
37
|
+
{
|
|
38
|
+
label: "silk",
|
|
39
|
+
value: "silk",
|
|
40
|
+
detail: "Everything in standard, plus the managed markdownlint config"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
label: "standard",
|
|
44
|
+
value: "standard",
|
|
45
|
+
detail: "Biome, shell-script hygiene hooks and markdown"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: "minimal",
|
|
49
|
+
value: "minimal",
|
|
50
|
+
detail: "Biome only"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}), {
|
|
54
|
+
flag: "lint-preset",
|
|
55
|
+
otherwise: DEFAULT_LINT_PRESET
|
|
56
|
+
})));
|
|
57
|
+
/**
|
|
58
|
+
* The files `savvy init --force` replaces wholesale, for the given config
|
|
59
|
+
* paths and preset.
|
|
60
|
+
*
|
|
61
|
+
* @remarks
|
|
62
|
+
* Only the files `--force` rewrites from scratch; the managed sections it
|
|
63
|
+
* syncs into the other hooks are re-synced with or without `--force`.
|
|
64
|
+
*
|
|
65
|
+
* @internal
|
|
66
|
+
*/
|
|
67
|
+
function forceTargets(opts) {
|
|
68
|
+
return [
|
|
69
|
+
".changeset/config.json",
|
|
70
|
+
".changeset/.markdownlint.json",
|
|
71
|
+
HUSKY_HOOK_PATH,
|
|
72
|
+
opts.commitConfig,
|
|
73
|
+
Lint.HUSKY_HOOK_PATH,
|
|
74
|
+
...opts.lintPreset === "silk" ? [Lint.MARKDOWNLINT_CONFIG_PATH] : [],
|
|
75
|
+
opts.lintConfig
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Whether `savvy init` may go ahead: asks before `--force` overwrites files
|
|
80
|
+
* that already exist.
|
|
81
|
+
*
|
|
82
|
+
* @remarks
|
|
83
|
+
* Asks only when `force` is set and at least one of `targets` exists; `--yes`
|
|
84
|
+
* and a run that cannot prompt proceed without asking, as `init --force`
|
|
85
|
+
* always has. A "no" prints a line saying nothing was changed and answers
|
|
86
|
+
* `false`.
|
|
87
|
+
*
|
|
88
|
+
* @internal
|
|
89
|
+
*/
|
|
90
|
+
function confirmForce(opts) {
|
|
91
|
+
return Effect.gen(function* () {
|
|
92
|
+
if (!opts.force) return true;
|
|
93
|
+
const fs = yield* FileSystem.FileSystem;
|
|
94
|
+
const existing = yield* Effect.filter(opts.targets, (path) => fs.exists(path).pipe(Effect.orElseSucceed(() => false)));
|
|
95
|
+
if (existing.length === 0) return true;
|
|
96
|
+
const confirmed = yield* confirmDestructive({
|
|
97
|
+
message: `Overwrite ${existing.length} existing file(s) with --force? (${existing.join(", ")})`,
|
|
98
|
+
yes: opts.yes
|
|
99
|
+
});
|
|
100
|
+
if (!confirmed) yield* Report.print([Report.skip("Nothing changed: existing files kept")]);
|
|
101
|
+
return confirmed;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The failure a step of `savvy init` stopped with, as a `CommandError` that
|
|
106
|
+
* draws itself; a `CommandError` a step already raised passes through.
|
|
107
|
+
*
|
|
108
|
+
* @internal
|
|
109
|
+
*/
|
|
110
|
+
function initFailure(error) {
|
|
111
|
+
return error instanceof CommandError ? error : CommandError.from(error, {
|
|
112
|
+
message: "savvy init stopped before finishing",
|
|
113
|
+
hint: "Fix the problem above and re-run savvy init; the steps that already ran are safe to repeat."
|
|
114
|
+
});
|
|
115
|
+
}
|
|
36
116
|
/**
|
|
37
117
|
* Run the three init step Effects in order: changeset → commit → lint.
|
|
38
118
|
*
|
|
@@ -53,25 +133,33 @@ function runInit(steps) {
|
|
|
53
133
|
/* v8 ignore start -- CLI registration; orchestration logic tested via runInit */
|
|
54
134
|
const _initCommand = Command.make("init", {
|
|
55
135
|
force: forceOption,
|
|
136
|
+
yes: yesFlag,
|
|
56
137
|
commitConfig: commitConfigOption,
|
|
57
138
|
lintConfig: lintConfigOption,
|
|
58
139
|
lintPreset: lintPresetOption
|
|
59
|
-
}, (opts) =>
|
|
60
|
-
|
|
61
|
-
force: opts.force,
|
|
62
|
-
quiet: false,
|
|
63
|
-
skipMarkdownlint: false,
|
|
64
|
-
check: false
|
|
65
|
-
}),
|
|
66
|
-
commit: runCommitInit({
|
|
67
|
-
force: opts.force,
|
|
68
|
-
config: opts.commitConfig
|
|
69
|
-
}),
|
|
70
|
-
lint: runLintInit({
|
|
140
|
+
}, (opts) => Effect.gen(function* () {
|
|
141
|
+
if (!(yield* confirmForce({
|
|
71
142
|
force: opts.force,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
})
|
|
143
|
+
yes: opts.yes,
|
|
144
|
+
targets: forceTargets(opts)
|
|
145
|
+
}))) return;
|
|
146
|
+
yield* runInit({
|
|
147
|
+
changeset: runChangesetInit({
|
|
148
|
+
force: opts.force,
|
|
149
|
+
quiet: false,
|
|
150
|
+
skipMarkdownlint: false,
|
|
151
|
+
check: false
|
|
152
|
+
}),
|
|
153
|
+
commit: runCommitInit({
|
|
154
|
+
force: opts.force,
|
|
155
|
+
config: opts.commitConfig
|
|
156
|
+
}),
|
|
157
|
+
lint: runLintInit({
|
|
158
|
+
force: opts.force,
|
|
159
|
+
config: opts.lintConfig,
|
|
160
|
+
preset: opts.lintPreset
|
|
161
|
+
})
|
|
162
|
+
}).pipe(Effect.mapError(initFailure));
|
|
75
163
|
})).pipe(Command.withDescription("Bootstrap a repository for all Silk Suite tools in one pass"));
|
|
76
164
|
/* v8 ignore stop */
|
|
77
165
|
/**
|
|
@@ -85,4 +173,4 @@ const _initCommand = Command.make("init", {
|
|
|
85
173
|
const initCommand = _initCommand;
|
|
86
174
|
|
|
87
175
|
//#endregion
|
|
88
|
-
export { initCommand, runInit };
|
|
176
|
+
export { confirmForce, forceTargets, initCommand, initFailure, lintPresetOption, runInit };
|
package/commands/lint/check.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Report } from "../../internal/report.js";
|
|
2
|
+
import { hookTable } from "../check-section.js";
|
|
2
3
|
import { BIOME_VERSION } from "./biome-version.js";
|
|
4
|
+
import { CliExit } from "@effected/cli";
|
|
3
5
|
import { ConfigDiscovery, Lint, SavvyBaseSection, SavvyHooksSection, SavvyToolchainSection, savvyBasePreamble, savvyHooksHygiene, savvyInstallBlock, savvyOkfBlock, savvyToolchainCheck } from "@savvy-web/silk-effects";
|
|
4
6
|
import { Effect, FileSystem, Option } from "effect";
|
|
5
7
|
import { Jsonc } from "@effected/jsonc";
|
|
@@ -132,17 +134,25 @@ function checkBiomeSchemas() {
|
|
|
132
134
|
});
|
|
133
135
|
}
|
|
134
136
|
/**
|
|
135
|
-
*
|
|
137
|
+
* Check the lint-staged setup and return it as a {@link CheckSection}.
|
|
136
138
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
+
* @remarks
|
|
140
|
+
* Severity rule: anything `savvy init` would write or rewrite is a finding
|
|
141
|
+
* (`✗`, verdict `failure`) — a missing config file or `pre-commit` hook, a
|
|
142
|
+
* managed section that is absent or drifted in a hook file that exists, a
|
|
143
|
+
* biome `$schema` behind {@link BIOME_VERSION}, a markdownlint `$schema` that
|
|
144
|
+
* differs from the template. Advice is `⚠` (verdict `warning`): markdownlint
|
|
145
|
+
* rules that differ from the template (only `savvy init --force` overwrites
|
|
146
|
+
* them, so they may be deliberate) and biome configs that could not be read.
|
|
147
|
+
* Absent is not a finding where a preset may leave it out: a missing hygiene
|
|
148
|
+
* hook FILE (the minimal preset writes none) or an uninstalled tool is `↷`.
|
|
139
149
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
150
|
+
* Under `quiet` the body is only the collected warning lines (none when
|
|
151
|
+
* there are none); the verdict is the same.
|
|
142
152
|
*
|
|
143
153
|
* @internal
|
|
144
154
|
*/
|
|
145
|
-
function
|
|
155
|
+
function lintCheckSection(opts) {
|
|
146
156
|
const { quiet } = opts;
|
|
147
157
|
return Effect.gen(function* () {
|
|
148
158
|
const fs = yield* FileSystem.FileSystem;
|
|
@@ -150,173 +160,115 @@ function runLintCheck(opts) {
|
|
|
150
160
|
const td = yield* ToolDiscovery;
|
|
151
161
|
const discovery = yield* ConfigDiscovery;
|
|
152
162
|
const warnings = [];
|
|
163
|
+
const rows = [];
|
|
164
|
+
const stateOf = (outcome) => CheckOutcome.$is("UpToDate")(outcome) ? "up-to-date" : CheckOutcome.$is("Drifted")(outcome) ? "outdated" : "missing";
|
|
153
165
|
const foundConfig = yield* findConfigFile(fs);
|
|
154
166
|
const hasHuskyHook = yield* fs.exists(Lint.HUSKY_HOOK_PATH);
|
|
155
|
-
let sectionsHealthy = true;
|
|
156
|
-
let baseStatusLabel = "missing";
|
|
157
|
-
let lintStatusLabel = "missing";
|
|
158
|
-
let okfStatusLabel = "missing";
|
|
159
167
|
let detectedConfigPath = null;
|
|
160
168
|
if (hasHuskyHook) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (!upToDate) sectionsHealthy = false;
|
|
167
|
-
}
|
|
169
|
+
rows.push([
|
|
170
|
+
Lint.HUSKY_HOOK_PATH,
|
|
171
|
+
"savvy-base",
|
|
172
|
+
stateOf(yield* ms.check(Lint.HUSKY_HOOK_PATH, SavvyBaseSection.section(savvyBasePreamble())))
|
|
173
|
+
]);
|
|
168
174
|
const existing = yield* ms.read(Lint.HUSKY_HOOK_PATH, Lint.SavvyLintSectionDef);
|
|
169
175
|
if (Option.isSome(existing)) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
else {
|
|
190
|
-
const upToDate = CheckOutcome.$is("UpToDate")(okfResult);
|
|
191
|
-
okfStatusLabel = upToDate ? "up-to-date" : "outdated";
|
|
192
|
-
if (!upToDate) sectionsHealthy = false;
|
|
193
|
-
}
|
|
194
|
-
if (baseStatusLabel !== "up-to-date" || lintStatusLabel !== "up-to-date" || okfStatusLabel !== "up-to-date") warnings.push(`${WARNING} Your ${Lint.HUSKY_HOOK_PATH} managed sections are out of date.\n Run 'savvy init' to update (preserves your custom hooks).`);
|
|
195
|
-
} else {
|
|
196
|
-
sectionsHealthy = false;
|
|
197
|
-
warnings.push(`${WARNING} No husky pre-commit hook found.\n Run 'savvy init' to create it.`);
|
|
198
|
-
}
|
|
176
|
+
detectedConfigPath = extractConfigPathFromManaged(existing.value.content);
|
|
177
|
+
const state = detectedConfigPath ? CheckOutcome.$is("UpToDate")(yield* ms.check(Lint.HUSKY_HOOK_PATH, Lint.savvyLintBlock(detectedConfigPath))) ? "up-to-date" : "outdated" : "outdated";
|
|
178
|
+
rows.push([
|
|
179
|
+
Lint.HUSKY_HOOK_PATH,
|
|
180
|
+
"savvy-lint",
|
|
181
|
+
state
|
|
182
|
+
]);
|
|
183
|
+
} else rows.push([
|
|
184
|
+
Lint.HUSKY_HOOK_PATH,
|
|
185
|
+
"savvy-lint",
|
|
186
|
+
"missing"
|
|
187
|
+
]);
|
|
188
|
+
rows.push([
|
|
189
|
+
Lint.HUSKY_HOOK_PATH,
|
|
190
|
+
"savvy-okf",
|
|
191
|
+
stateOf(yield* ms.check(Lint.HUSKY_HOOK_PATH, savvyOkfBlock()))
|
|
192
|
+
]);
|
|
193
|
+
if (rows.some(([, , state]) => state !== "up-to-date")) warnings.push(`${WARNING} Your ${Lint.HUSKY_HOOK_PATH} managed sections are out of date.\n Run 'savvy init' to update (preserves your custom hooks).`);
|
|
194
|
+
} else warnings.push(`${WARNING} No husky pre-commit hook found.\n Run 'savvy init' to create it.`);
|
|
199
195
|
if (!foundConfig) warnings.push(`${WARNING} No lint-staged config file found.\n Run 'savvy init' to create one.`);
|
|
200
|
-
const
|
|
196
|
+
for (const hookPath of [
|
|
201
197
|
Lint.POST_CHECKOUT_HOOK_PATH,
|
|
202
198
|
Lint.POST_MERGE_HOOK_PATH,
|
|
203
199
|
Lint.POST_COMMIT_HOOK_PATH
|
|
204
|
-
]
|
|
205
|
-
const shellHookStatuses = [];
|
|
206
|
-
const toolchainStatuses = [];
|
|
207
|
-
const installStatuses = [];
|
|
208
|
-
for (const hookPath of shellHookPaths) {
|
|
200
|
+
]) {
|
|
209
201
|
if (!(yield* fs.exists(hookPath))) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
202
|
+
rows.push([
|
|
203
|
+
hookPath,
|
|
204
|
+
"(hook file)",
|
|
205
|
+
"not installed"
|
|
206
|
+
]);
|
|
215
207
|
continue;
|
|
216
208
|
}
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
path: hookPath,
|
|
222
|
-
found,
|
|
223
|
-
isUpToDate
|
|
224
|
-
});
|
|
225
|
-
if (!found) {
|
|
226
|
-
sectionsHealthy = false;
|
|
227
|
-
warnings.push(`${WARNING} ${hookPath} has no savvy-hooks section.\n Run 'savvy init' to add it.`);
|
|
228
|
-
} else if (!isUpToDate) {
|
|
229
|
-
sectionsHealthy = false;
|
|
230
|
-
warnings.push(`${WARNING} ${hookPath} savvy-hooks section is outdated.\n Run 'savvy init' to update.`);
|
|
209
|
+
const sections = [["savvy-hooks", SavvyHooksSection.section(savvyHooksHygiene())]];
|
|
210
|
+
if (hookPath !== Lint.POST_COMMIT_HOOK_PATH) {
|
|
211
|
+
const installHook = hookPath === Lint.POST_CHECKOUT_HOOK_PATH ? "post-checkout" : "post-merge";
|
|
212
|
+
sections.push(["savvy-install", savvyInstallBlock(installHook)], ["savvy-toolchain", SavvyToolchainSection.section(savvyToolchainCheck())]);
|
|
231
213
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
});
|
|
242
|
-
if (!installFound) {
|
|
243
|
-
sectionsHealthy = false;
|
|
244
|
-
warnings.push(`${WARNING} ${hookPath} has no savvy-install section.\n Run 'savvy init' to add it.`);
|
|
245
|
-
} else if (!installUpToDate) {
|
|
246
|
-
sectionsHealthy = false;
|
|
247
|
-
warnings.push(`${WARNING} ${hookPath} savvy-install section is outdated.\n Run 'savvy init' to update.`);
|
|
248
|
-
}
|
|
249
|
-
const toolchainResult = yield* ms.check(hookPath, SavvyToolchainSection.section(savvyToolchainCheck()));
|
|
250
|
-
const toolchainFound = !CheckOutcome.$is("Absent")(toolchainResult);
|
|
251
|
-
const toolchainUpToDate = CheckOutcome.$is("UpToDate")(toolchainResult);
|
|
252
|
-
toolchainStatuses.push({
|
|
253
|
-
path: hookPath,
|
|
254
|
-
found: toolchainFound,
|
|
255
|
-
isUpToDate: toolchainUpToDate
|
|
256
|
-
});
|
|
257
|
-
if (!toolchainFound) {
|
|
258
|
-
sectionsHealthy = false;
|
|
259
|
-
warnings.push(`${WARNING} ${hookPath} has no savvy-toolchain section.\n Run 'savvy init' to add it.`);
|
|
260
|
-
} else if (!toolchainUpToDate) {
|
|
261
|
-
sectionsHealthy = false;
|
|
262
|
-
warnings.push(`${WARNING} ${hookPath} savvy-toolchain section is outdated.\n Run 'savvy init' to update.`);
|
|
214
|
+
for (const [name, section] of sections) {
|
|
215
|
+
const state = stateOf(yield* ms.check(hookPath, section));
|
|
216
|
+
rows.push([
|
|
217
|
+
hookPath,
|
|
218
|
+
name,
|
|
219
|
+
state
|
|
220
|
+
]);
|
|
221
|
+
if (state === "missing") warnings.push(`${WARNING} ${hookPath} has no ${name} section.\n Run 'savvy init' to add it.`);
|
|
222
|
+
else if (state === "outdated") warnings.push(`${WARNING} ${hookPath} ${name} section is outdated.\n Run 'savvy init' to update.`);
|
|
263
223
|
}
|
|
264
224
|
}
|
|
265
|
-
const biomeSchemaStatus = yield* checkBiomeSchemas().pipe(Effect.
|
|
225
|
+
const biomeSchemaStatus = yield* checkBiomeSchemas().pipe(Effect.map((status) => ({
|
|
226
|
+
...status,
|
|
227
|
+
readable: true
|
|
228
|
+
})), Effect.catch(() => Effect.succeed({
|
|
266
229
|
statuses: [],
|
|
267
|
-
warnings: [`${WARNING} Could not check biome $schema URLs.`]
|
|
230
|
+
warnings: [`${WARNING} Could not check biome $schema URLs.`],
|
|
231
|
+
readable: false
|
|
268
232
|
})));
|
|
269
233
|
warnings.push(...biomeSchemaStatus.warnings);
|
|
270
234
|
const hasMarkdownlintConfig = yield* fs.exists(Lint.MARKDOWNLINT_CONFIG_PATH);
|
|
271
|
-
|
|
235
|
+
const markdownlintStatus = hasMarkdownlintConfig ? yield* checkMarkdownlintConfig(yield* fs.readFileString(Lint.MARKDOWNLINT_CONFIG_PATH)) : {
|
|
272
236
|
exists: false,
|
|
273
237
|
schemaMatches: false,
|
|
274
238
|
configMatches: false,
|
|
275
239
|
isUpToDate: false
|
|
276
240
|
};
|
|
241
|
+
if (hasMarkdownlintConfig && !markdownlintStatus.schemaMatches) warnings.push(`${WARNING} ${Lint.MARKDOWNLINT_CONFIG_PATH}: $schema differs from template.\n Run 'savvy init' to update it.`);
|
|
242
|
+
if (hasMarkdownlintConfig && !markdownlintStatus.configMatches) warnings.push(`${WARNING} ${Lint.MARKDOWNLINT_CONFIG_PATH}: config rules differ from template.\n Run 'savvy init --force' to overwrite.`);
|
|
243
|
+
const sectionsHealthy = rows.every(([, , state]) => state === "up-to-date" || state === "not installed");
|
|
244
|
+
const findings = !foundConfig || !hasHuskyHook || !sectionsHealthy || hasMarkdownlintConfig && !markdownlintStatus.schemaMatches || biomeSchemaStatus.statuses.some((status) => !status.matches);
|
|
245
|
+
const advice = hasMarkdownlintConfig && !markdownlintStatus.configMatches || !biomeSchemaStatus.readable;
|
|
246
|
+
const verdict = findings ? "failure" : advice ? "warning" : "success";
|
|
247
|
+
const title = "lint-staged";
|
|
248
|
+
if (quiet) return {
|
|
249
|
+
title,
|
|
250
|
+
blocks: warnings.map((warning) => Report.verbatim(warning)),
|
|
251
|
+
verdict,
|
|
252
|
+
fixedByInit: findings
|
|
253
|
+
};
|
|
254
|
+
const blocks = [];
|
|
255
|
+
blocks.push(foundConfig ? Report.ok(`Config file: ${foundConfig}`) : Report.fail("No lint-staged config file found"));
|
|
256
|
+
blocks.push(hasHuskyHook ? Report.ok(`Husky hook: ${Lint.HUSKY_HOOK_PATH}`) : Report.fail("No husky pre-commit hook found"));
|
|
257
|
+
if (detectedConfigPath) blocks.push(Report.detail(`savvy-lint runs: ${detectedConfigPath}`));
|
|
258
|
+
if (rows.length > 0) blocks.push(hookTable(rows));
|
|
277
259
|
if (hasMarkdownlintConfig) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (foundConfig) yield* Output.ok(`Config file: ${foundConfig}`);
|
|
288
|
-
else yield* Output.fail(`No lint-staged config file found`);
|
|
289
|
-
if (hasHuskyHook) yield* Output.ok(`Husky hook: ${Lint.HUSKY_HOOK_PATH}`);
|
|
290
|
-
else yield* Output.fail(`No husky pre-commit hook found`);
|
|
291
|
-
if (hasHuskyHook) {
|
|
292
|
-
if (baseStatusLabel === "up-to-date") yield* Output.ok(`Base section: up-to-date`);
|
|
293
|
-
else if (baseStatusLabel === "outdated") yield* Output.warn(`Base section: outdated (run 'savvy init' to update)`);
|
|
294
|
-
else yield* Output.skip(`Base section: not found (run 'savvy init' to add)`);
|
|
295
|
-
const lintLabel = detectedConfigPath ? ` (config: ${detectedConfigPath})` : "";
|
|
296
|
-
if (lintStatusLabel === "up-to-date") yield* Output.ok(`Lint section: up-to-date${lintLabel}`);
|
|
297
|
-
else if (lintStatusLabel === "outdated") yield* Output.warn(`Lint section: outdated (run 'savvy init' to update)`);
|
|
298
|
-
else yield* Output.skip(`Lint section: not found (run 'savvy init' to add)`);
|
|
299
|
-
if (okfStatusLabel === "up-to-date") yield* Output.ok(`OKF section: up-to-date`);
|
|
300
|
-
else if (okfStatusLabel === "outdated") yield* Output.warn(`OKF section: outdated (run 'savvy init' to update)`);
|
|
301
|
-
else yield* Output.skip(`OKF section: not found (run 'savvy init' to add)`);
|
|
302
|
-
}
|
|
303
|
-
for (const status of shellHookStatuses) if (!status.found) yield* Output.skip(`${status.path}: savvy-hooks section not found`);
|
|
304
|
-
else if (status.isUpToDate) yield* Output.ok(`${status.path}: up-to-date`);
|
|
305
|
-
else yield* Output.warn(`${status.path}: outdated (run 'savvy init' to update)`);
|
|
306
|
-
for (const status of installStatuses) if (!status.found) yield* Output.skip(`${status.path}: savvy-install section not found`);
|
|
307
|
-
else if (status.isUpToDate) yield* Output.ok(`${status.path}: savvy-install up-to-date`);
|
|
308
|
-
else yield* Output.warn(`${status.path}: savvy-install outdated (run 'savvy init' to update)`);
|
|
309
|
-
for (const status of toolchainStatuses) if (!status.found) yield* Output.skip(`${status.path}: savvy-toolchain section not found`);
|
|
310
|
-
else if (status.isUpToDate) yield* Output.ok(`${status.path}: savvy-toolchain up-to-date`);
|
|
311
|
-
else yield* Output.warn(`${status.path}: savvy-toolchain outdated (run 'savvy init' to update)`);
|
|
312
|
-
yield* Output.line("");
|
|
313
|
-
yield* Output.heading("Tool availability");
|
|
260
|
+
if (markdownlintStatus.isUpToDate) blocks.push(Report.ok(`${Lint.MARKDOWNLINT_CONFIG_PATH}: up-to-date`));
|
|
261
|
+
else {
|
|
262
|
+
if (!markdownlintStatus.schemaMatches) blocks.push(Report.fail(`${Lint.MARKDOWNLINT_CONFIG_PATH}: $schema differs from template`));
|
|
263
|
+
if (!markdownlintStatus.configMatches) blocks.push(Report.warn(`${Lint.MARKDOWNLINT_CONFIG_PATH}: config rules differ from template (savvy init --force overwrites)`));
|
|
264
|
+
}
|
|
265
|
+
} else blocks.push(Report.skip(`${Lint.MARKDOWNLINT_CONFIG_PATH}: not found`));
|
|
266
|
+
if (!biomeSchemaStatus.readable) blocks.push(Report.warn("Could not check biome $schema URLs"));
|
|
267
|
+
for (const status of biomeSchemaStatus.statuses) blocks.push(status.matches ? Report.ok(`${status.path}: biome $schema up-to-date`) : Report.fail(`${status.path}: biome $schema outdated`));
|
|
268
|
+
blocks.push(Report.line("Tool availability"));
|
|
314
269
|
const biomeAvailable = yield* td.isAvailable(Tool.named("biome"));
|
|
315
270
|
const biomeConfig = yield* findConfig(discovery, ["biome.jsonc", "biome.json"]);
|
|
316
|
-
|
|
317
|
-
const configInfo = biomeConfig ? ` (config: ${biomeConfig})` : "";
|
|
318
|
-
yield* Output.ok(`Biome${configInfo}`);
|
|
319
|
-
} else yield* Output.skip(`Biome: not installed`);
|
|
271
|
+
blocks.push(biomeAvailable ? Report.ok(`Biome${biomeConfig ? ` (config: ${biomeConfig})` : ""}`) : Report.skip("Biome: not installed"));
|
|
320
272
|
const markdownAvailable = yield* td.isAvailable(Tool.named("markdownlint-cli2"));
|
|
321
273
|
const markdownConfig = yield* findConfig(discovery, [
|
|
322
274
|
".markdownlint-cli2.jsonc",
|
|
@@ -327,33 +279,36 @@ function runLintCheck(opts) {
|
|
|
327
279
|
".markdownlint.json",
|
|
328
280
|
".markdownlint.yaml"
|
|
329
281
|
]);
|
|
330
|
-
|
|
331
|
-
const configInfo = markdownConfig ? ` (config: ${markdownConfig})` : "";
|
|
332
|
-
yield* Output.ok(`markdownlint-cli2${configInfo}`);
|
|
333
|
-
} else yield* Output.skip(`markdownlint-cli2: not installed`);
|
|
282
|
+
blocks.push(markdownAvailable ? Report.ok(`markdownlint-cli2${markdownConfig ? ` (config: ${markdownConfig})` : ""}`) : Report.skip("markdownlint-cli2: not installed"));
|
|
334
283
|
const tscAvailable = yield* td.isAvailable(Tool.named("tsc"));
|
|
335
|
-
const tsgoAvailable = yield* td.isAvailable(Tool.named("tsgo"));
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
284
|
+
const tsgoAvailable = tscAvailable ? false : yield* td.isAvailable(Tool.named("tsgo"));
|
|
285
|
+
blocks.push(tscAvailable ? Report.ok("TypeScript (tsc)") : tsgoAvailable ? Report.ok("TypeScript (tsgo)") : Report.skip("TypeScript: not installed"));
|
|
286
|
+
blocks.push(verdict === "failure" ? Report.fail("lint-staged needs configuration") : verdict === "warning" ? Report.warn("lint-staged is configured, with advisories") : Report.ok("lint-staged is configured correctly"));
|
|
287
|
+
return {
|
|
288
|
+
title,
|
|
289
|
+
blocks,
|
|
290
|
+
verdict,
|
|
291
|
+
fixedByInit: findings
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Check the lint-staged setup, print its report on stdout, and set exit code
|
|
297
|
+
* 1 when it found a misconfiguration (see {@link lintCheckSection} for the
|
|
298
|
+
* severity rule). Under `quiet` only the warnings are printed.
|
|
299
|
+
*
|
|
300
|
+
* @param opts - Options for the check command
|
|
301
|
+
* @returns An Effect that performs validation and prints its report
|
|
302
|
+
*
|
|
303
|
+
* @internal
|
|
304
|
+
*/
|
|
305
|
+
function runLintCheck(opts) {
|
|
306
|
+
return Effect.gen(function* () {
|
|
307
|
+
const section = yield* lintCheckSection(opts);
|
|
308
|
+
yield* Report.print(opts.quiet ? section.blocks : [Report.heading(section.title), ...section.blocks]);
|
|
309
|
+
if (section.verdict === "failure") yield* CliExit.set(1);
|
|
355
310
|
});
|
|
356
311
|
}
|
|
357
312
|
|
|
358
313
|
//#endregion
|
|
359
|
-
export { runLintCheck };
|
|
314
|
+
export { lintCheckSection, runLintCheck };
|