@savvy-web/cli 3.5.0 → 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/clean.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Report } from "../internal/report.js";
|
|
2
|
+
import { CommandError } from "../internal/command-error.js";
|
|
2
3
|
import { Command, Flag } from "effect/cli";
|
|
3
4
|
import { join, sep } from "node:path";
|
|
4
|
-
import {
|
|
5
|
+
import { CliInteractive } from "@effected/cli";
|
|
6
|
+
import { Console, Effect, PubSub } from "effect";
|
|
5
7
|
import { WorkspaceDiscovery } from "@effected/workspaces";
|
|
6
8
|
import { glob, realpath, rm } from "node:fs/promises";
|
|
9
|
+
import { CliUi } from "@effected/cli/ui";
|
|
7
10
|
|
|
8
11
|
//#region src/commands/clean.ts
|
|
9
12
|
/**
|
|
@@ -25,8 +28,6 @@ const DEFAULT_GLOBS = [
|
|
|
25
28
|
];
|
|
26
29
|
/** Directory names a recursive (`**`) glob must never descend into. */
|
|
27
30
|
const NO_DESCEND = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
28
|
-
/** Unexpected failure while planning or removing artifacts. */
|
|
29
|
-
var CleanError = class extends Data.TaggedError("CleanError") {};
|
|
30
31
|
/**
|
|
31
32
|
* Glob `patterns` at the top of `pkgPath`, classify dir vs file, and enforce
|
|
32
33
|
* that every match stays within `pkgPath` (rejecting symlink/`..` escapes and
|
|
@@ -58,9 +59,9 @@ function collectTargets(pkgPath, patterns) {
|
|
|
58
59
|
}
|
|
59
60
|
return [...seen.values()];
|
|
60
61
|
},
|
|
61
|
-
catch: (e) =>
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
catch: (e) => CommandError.from(e, {
|
|
63
|
+
message: `could not list the artifacts in ${pkgPath}`,
|
|
64
|
+
hint: "Check that the workspace directory exists and is readable, or narrow --globs."
|
|
64
65
|
})
|
|
65
66
|
});
|
|
66
67
|
}
|
|
@@ -103,16 +104,61 @@ function parseGlobs(raw) {
|
|
|
103
104
|
const parts = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
104
105
|
return parts.length > 0 ? parts : DEFAULT_GLOBS;
|
|
105
106
|
}
|
|
107
|
+
/** A workspace's display name: its relative path, `<root>` for the root. */
|
|
108
|
+
const groupName = (relativePath) => relativePath === "." ? "<root>" : relativePath;
|
|
109
|
+
/**
|
|
110
|
+
* One workspace's document: its name, then each removed target and each
|
|
111
|
+
* failure. Only targets that actually succeeded are listed as removed (in a
|
|
112
|
+
* dry run `removed` holds every target); a failure gets its own marker.
|
|
113
|
+
*/
|
|
114
|
+
const groupBlocks = (name, report, dryRun) => {
|
|
115
|
+
const verb = dryRun ? "would remove" : "removed";
|
|
116
|
+
return [
|
|
117
|
+
Report.line(""),
|
|
118
|
+
Report.heading(name),
|
|
119
|
+
...report.removed.map((t) => Report.detail(`${verb} [${t.kind}] ${t.path}`)),
|
|
120
|
+
...report.failed.map((f) => Report.warn(`failed [${f.target.kind}] ${f.target.path}: ${f.reason}`))
|
|
121
|
+
];
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Remove each workspace's targets in order (leaves first, the root last),
|
|
125
|
+
* calling `onStart` before and `onDone` after each workspace that has any.
|
|
126
|
+
*/
|
|
127
|
+
const removeGroups = (groups, dryRun, hooks) => Effect.gen(function* () {
|
|
128
|
+
let total = 0;
|
|
129
|
+
const failures = [];
|
|
130
|
+
for (const group of groups) {
|
|
131
|
+
if (group.targets.length === 0) continue;
|
|
132
|
+
yield* hooks.onStart(group);
|
|
133
|
+
const report = yield* removeTargets(group.targets, dryRun);
|
|
134
|
+
yield* hooks.onDone(group, report);
|
|
135
|
+
total += report.removed.length;
|
|
136
|
+
failures.push(...report.failed);
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
total,
|
|
140
|
+
failures
|
|
141
|
+
};
|
|
142
|
+
});
|
|
106
143
|
/**
|
|
107
144
|
* Discover packages, plan targets per workspace (leaves first, root last),
|
|
108
145
|
* dedup across overlapping roots, remove (or preview), and report.
|
|
146
|
+
*
|
|
147
|
+
* @remarks
|
|
148
|
+
* A person at a terminal (`CliInteractive`) watches a live progress view
|
|
149
|
+
* whose committed frame is the summary — the counts and a table of anything
|
|
150
|
+
* that could not be removed — with each workspace's document printed above
|
|
151
|
+
* it as the workspace finishes. Every other run (an agent, CI, a pipe)
|
|
152
|
+
* prints each workspace's document as it finishes and a one-line total, and
|
|
153
|
+
* never loads Ink or React. Either way, targets that could not be removed
|
|
154
|
+
* fail the run with a `CommandError` listing them.
|
|
109
155
|
*/
|
|
110
156
|
function runClean(opts) {
|
|
111
157
|
const patterns = parseGlobs(opts.globs);
|
|
112
158
|
return Effect.gen(function* () {
|
|
113
|
-
const packages = yield* (yield* WorkspaceDiscovery).listPackages().pipe(Effect.mapError((e) =>
|
|
114
|
-
|
|
115
|
-
|
|
159
|
+
const packages = yield* (yield* WorkspaceDiscovery).listPackages().pipe(Effect.mapError((e) => CommandError.from(e, {
|
|
160
|
+
message: "could not discover the workspace packages",
|
|
161
|
+
hint: "Run savvy clean from inside the workspace (a directory with pnpm-workspace.yaml or a workspaces field)."
|
|
116
162
|
})));
|
|
117
163
|
const leaves = packages.filter((p) => !(p.relativePath === "."));
|
|
118
164
|
const roots = packages.filter((p) => p.relativePath === ".");
|
|
@@ -122,47 +168,64 @@ function runClean(opts) {
|
|
|
122
168
|
targets
|
|
123
169
|
}))));
|
|
124
170
|
const seen = /* @__PURE__ */ new Set();
|
|
125
|
-
const groups = planned.map(({ pkg, targets }) => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
report
|
|
144
|
-
}))));
|
|
145
|
-
for (const { g, report } of reports) {
|
|
146
|
-
if (g.targets.length === 0) continue;
|
|
147
|
-
yield* Output.line("");
|
|
148
|
-
yield* Output.heading(g.pkg.relativePath === "." ? "<root>" : g.pkg.relativePath);
|
|
149
|
-
for (const t of report.removed) yield* Output.detail(`${verb} [${t.kind}] ${t.path}`);
|
|
150
|
-
for (const f of report.failed) yield* Output.warn(`failed [${f.target.kind}] ${f.target.path}: ${f.reason}`);
|
|
151
|
-
total += report.removed.length;
|
|
152
|
-
failures.push(...report.failed);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
yield* Output.line("");
|
|
156
|
-
yield* Output.ok(`${opts.dryRun ? "Would remove" : "Removed"} ${total} item(s)`);
|
|
157
|
-
if (failures.length > 0) {
|
|
158
|
-
for (const f of failures) yield* Effect.logError(`Failed to remove ${f.target.path}: ${f.reason}`);
|
|
159
|
-
return yield* Effect.fail(new CleanError({
|
|
160
|
-
step: "remove",
|
|
161
|
-
reason: `${failures.length} target(s) could not be removed`
|
|
162
|
-
}));
|
|
163
|
-
}
|
|
171
|
+
const groups = planned.map(({ pkg, targets }) => ({
|
|
172
|
+
name: groupName(pkg.relativePath),
|
|
173
|
+
targets: targets.filter((t) => {
|
|
174
|
+
if (seen.has(t.path)) return false;
|
|
175
|
+
seen.add(t.path);
|
|
176
|
+
return true;
|
|
177
|
+
})
|
|
178
|
+
}));
|
|
179
|
+
const printGroup = (group, report) => Report.print(groupBlocks(group.name, report, opts.dryRun));
|
|
180
|
+
const outcome = (yield* CliInteractive) ? yield* cleanLive(groups, opts.dryRun, printGroup) : yield* removeGroups(groups, opts.dryRun, {
|
|
181
|
+
onStart: () => Effect.void,
|
|
182
|
+
onDone: printGroup
|
|
183
|
+
}).pipe(Effect.tap(({ total }) => Report.print([Report.line(""), Report.ok(`${opts.dryRun ? "Would remove" : "Removed"} ${total} item(s)`)])));
|
|
184
|
+
if (outcome.failures.length > 0) return yield* Effect.fail(new CommandError({
|
|
185
|
+
message: `${outcome.failures.length} target(s) could not be removed`,
|
|
186
|
+
detail: outcome.failures.map((f) => `${f.target.path}: ${f.reason}`),
|
|
187
|
+
hint: "Check the permissions on those paths and that no process holds them open, then re-run savvy clean."
|
|
188
|
+
}));
|
|
164
189
|
});
|
|
165
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* The removal pass under a live view, for a person at a terminal.
|
|
193
|
+
*
|
|
194
|
+
* @remarks
|
|
195
|
+
* The view's module (the JSX) is imported here, on the one path that draws,
|
|
196
|
+
* so no other run loads React. Every line the pass writes goes through the
|
|
197
|
+
* view's `logConsole`, landing above the frame instead of tearing it.
|
|
198
|
+
*/
|
|
199
|
+
const cleanLive = (groups, dryRun, printGroup) => Effect.scoped(Effect.gen(function* () {
|
|
200
|
+
const { cleanView } = yield* Effect.promise(() => import("./clean/view.js"));
|
|
201
|
+
const pubsub = yield* PubSub.unbounded();
|
|
202
|
+
const events = yield* PubSub.subscribe(pubsub);
|
|
203
|
+
const view = yield* CliUi.live({
|
|
204
|
+
...cleanView,
|
|
205
|
+
events
|
|
206
|
+
});
|
|
207
|
+
const workspaces = groups.filter((g) => g.targets.length > 0).length;
|
|
208
|
+
yield* PubSub.publish(pubsub, {
|
|
209
|
+
_tag: "Started",
|
|
210
|
+
workspaces,
|
|
211
|
+
dryRun
|
|
212
|
+
});
|
|
213
|
+
const outcome = yield* removeGroups(groups, dryRun, {
|
|
214
|
+
onStart: (group) => PubSub.publish(pubsub, {
|
|
215
|
+
_tag: "WorkspaceStarted",
|
|
216
|
+
name: group.name
|
|
217
|
+
}),
|
|
218
|
+
onDone: (group, report) => Effect.andThen(printGroup(group, report), PubSub.publish(pubsub, {
|
|
219
|
+
_tag: "WorkspaceDone",
|
|
220
|
+
name: group.name,
|
|
221
|
+
removed: report.removed,
|
|
222
|
+
failed: report.failed
|
|
223
|
+
}))
|
|
224
|
+
}).pipe(Effect.provideService(Console.Console, view.logConsole));
|
|
225
|
+
yield* PubSub.publish(pubsub, { _tag: "Ended" });
|
|
226
|
+
yield* view.close;
|
|
227
|
+
return outcome;
|
|
228
|
+
}));
|
|
166
229
|
/* v8 ignore start -- CLI option/registration; orchestration tested via runClean */
|
|
167
230
|
const globsOption = Flag.String("globs").pipe(Flag.withAlias("g"), Flag.withDescription(`Comma-separated glob patterns to remove from each workspace root (default: ${DEFAULT_GLOBS.join(",")})`), Flag.withDefault(DEFAULT_GLOBS.join(",")));
|
|
168
231
|
const dryRunOption = Flag.Boolean("dry-run").pipe(Flag.withAlias("n"), Flag.withDescription("Report what would be removed without deleting anything"), Flag.withDefault(false));
|
|
@@ -182,4 +245,4 @@ const _cleanCommand = Command.make("clean", {
|
|
|
182
245
|
const cleanCommand = _cleanCommand;
|
|
183
246
|
|
|
184
247
|
//#endregion
|
|
185
|
-
export {
|
|
248
|
+
export { cleanCommand, collectTargets, parseGlobs, removeTargets, runClean };
|
package/commands/commit/check.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Report } from "../../internal/report.js";
|
|
2
|
+
import { hookTable } from "../check-section.js";
|
|
2
3
|
import { HUSKY_HOOK_PATH, POST_CHECKOUT_HOOK_PATH, POST_COMMIT_HOOK_PATH, POST_MERGE_HOOK_PATH } from "./constants.js";
|
|
3
4
|
import { SECTION_DEF, savvyCommitBlock } from "./init.js";
|
|
5
|
+
import { CliExit } from "@effected/cli";
|
|
4
6
|
import { ChangesetConfigReader, Commitlint, SavvyBaseSection, SavvyHooksSection, SavvyToolchainSection, savvyBasePreamble, savvyHooksHygiene, savvyInstallBlock, savvyToolchainCheck } from "@savvy-web/silk-effects";
|
|
5
7
|
import { Effect, FileSystem, Option } from "effect";
|
|
6
8
|
import { VersioningStrategy } from "@effected/workspaces";
|
|
@@ -82,55 +84,47 @@ const detectReleaseFormat = Effect.gen(function* () {
|
|
|
82
84
|
return STRATEGY_TO_FORMAT[strategy.type] ?? "semver";
|
|
83
85
|
});
|
|
84
86
|
/**
|
|
85
|
-
*
|
|
87
|
+
* Check the commitlint setup and return it as a {@link CheckSection}.
|
|
86
88
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
89
|
+
* @remarks
|
|
90
|
+
* Severity rule: anything `savvy init` would write or rewrite — a missing
|
|
91
|
+
* config file or `commit-msg` hook, a hook file or managed section that is
|
|
92
|
+
* absent or drifted — is a finding (`✗`, verdict `failure`). A missing DCO
|
|
93
|
+
* file is advisory (`↷`): signoff is simply not required.
|
|
91
94
|
*
|
|
92
95
|
* @internal
|
|
93
96
|
*/
|
|
94
|
-
function
|
|
97
|
+
function commitCheckSection() {
|
|
95
98
|
return Effect.gen(function* () {
|
|
96
99
|
const fs = yield* FileSystem.FileSystem;
|
|
97
100
|
const ms = yield* ManagedSection;
|
|
98
|
-
|
|
101
|
+
const blocks = [];
|
|
102
|
+
const rows = [];
|
|
103
|
+
const stateOf = (outcome) => CheckOutcome.$is("UpToDate")(outcome) ? "up-to-date" : CheckOutcome.$is("Drifted")(outcome) ? "outdated" : "missing";
|
|
99
104
|
const foundConfig = yield* findConfigFile(fs);
|
|
100
|
-
|
|
101
|
-
else yield* Output.fail(`No commitlint config file found`);
|
|
105
|
+
blocks.push(foundConfig ? Report.ok(`Config file: ${foundConfig}`) : Report.fail("No commitlint config file found"));
|
|
102
106
|
const hasHuskyHook = yield* fs.exists(HUSKY_HOOK_PATH);
|
|
103
|
-
|
|
104
|
-
else yield* Output.fail(`No husky commit-msg hook found`);
|
|
105
|
-
let sectionsHealthy = true;
|
|
107
|
+
blocks.push(hasHuskyHook ? Report.ok(`Husky hook: ${HUSKY_HOOK_PATH}`) : Report.fail("No husky commit-msg hook found"));
|
|
106
108
|
if (hasHuskyHook) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
} else {
|
|
113
|
-
sectionsHealthy = false;
|
|
114
|
-
yield* Output.skip(`Base section: not found (run 'savvy init' to add)`);
|
|
115
|
-
}
|
|
109
|
+
rows.push([
|
|
110
|
+
HUSKY_HOOK_PATH,
|
|
111
|
+
"savvy-base",
|
|
112
|
+
stateOf(yield* ms.check(HUSKY_HOOK_PATH, SavvyBaseSection.section(savvyBasePreamble())))
|
|
113
|
+
]);
|
|
116
114
|
const block = yield* ms.read(HUSKY_HOOK_PATH, SECTION_DEF);
|
|
117
115
|
if (Option.isSome(block)) {
|
|
118
116
|
const configPath = extractConfigPathFromManaged(block.value.content);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
} else {
|
|
131
|
-
sectionsHealthy = false;
|
|
132
|
-
yield* Output.skip(`Commit section: not found (run 'savvy init' to add)`);
|
|
133
|
-
}
|
|
117
|
+
const state = configPath ? CheckOutcome.$is("UpToDate")(yield* ms.check(".husky/commit-msg", savvyCommitBlock(configPath))) ? "up-to-date" : "outdated" : "outdated";
|
|
118
|
+
rows.push([
|
|
119
|
+
HUSKY_HOOK_PATH,
|
|
120
|
+
"savvy-commit",
|
|
121
|
+
state
|
|
122
|
+
]);
|
|
123
|
+
} else rows.push([
|
|
124
|
+
HUSKY_HOOK_PATH,
|
|
125
|
+
"savvy-commit",
|
|
126
|
+
"missing"
|
|
127
|
+
]);
|
|
134
128
|
}
|
|
135
129
|
for (const hookPath of [
|
|
136
130
|
POST_CHECKOUT_HOOK_PATH,
|
|
@@ -138,55 +132,64 @@ function runCommitCheck() {
|
|
|
138
132
|
POST_COMMIT_HOOK_PATH
|
|
139
133
|
]) {
|
|
140
134
|
if (!(yield* fs.exists(hookPath))) {
|
|
141
|
-
|
|
142
|
-
|
|
135
|
+
rows.push([
|
|
136
|
+
hookPath,
|
|
137
|
+
"(hook file)",
|
|
138
|
+
"missing"
|
|
139
|
+
]);
|
|
143
140
|
continue;
|
|
144
141
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
} else {
|
|
151
|
-
sectionsHealthy = false;
|
|
152
|
-
yield* Output.skip(`Hygiene hook: ${hookPath} section not found (run 'savvy init' to add)`);
|
|
153
|
-
}
|
|
142
|
+
rows.push([
|
|
143
|
+
hookPath,
|
|
144
|
+
"savvy-hooks",
|
|
145
|
+
stateOf(yield* ms.check(hookPath, SavvyHooksSection.section(savvyHooksHygiene())))
|
|
146
|
+
]);
|
|
154
147
|
if (hookPath === ".husky/post-commit") continue;
|
|
155
148
|
const installHook = hookPath === ".husky/post-checkout" ? "post-checkout" : "post-merge";
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (CheckOutcome.$is("UpToDate")(toolchainStatus)) yield* Output.ok(`Toolchain check: ${hookPath}`);
|
|
167
|
-
else if (CheckOutcome.$is("Drifted")(toolchainStatus)) {
|
|
168
|
-
sectionsHealthy = false;
|
|
169
|
-
yield* Output.warn(`Toolchain check: ${hookPath} outdated (run 'savvy init' to update)`);
|
|
170
|
-
} else {
|
|
171
|
-
sectionsHealthy = false;
|
|
172
|
-
yield* Output.skip(`Toolchain check: ${hookPath} section not found (run 'savvy init' to add)`);
|
|
173
|
-
}
|
|
149
|
+
rows.push([
|
|
150
|
+
hookPath,
|
|
151
|
+
"savvy-install",
|
|
152
|
+
stateOf(yield* ms.check(hookPath, savvyInstallBlock(installHook)))
|
|
153
|
+
]);
|
|
154
|
+
rows.push([
|
|
155
|
+
hookPath,
|
|
156
|
+
"savvy-toolchain",
|
|
157
|
+
stateOf(yield* ms.check(hookPath, SavvyToolchainSection.section(savvyToolchainCheck())))
|
|
158
|
+
]);
|
|
174
159
|
}
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
yield* Output.heading("Detected settings");
|
|
179
|
-
yield* Output.detail(`DCO required: ${Commitlint.detectDCO()}`);
|
|
160
|
+
if (rows.length > 0) blocks.push(hookTable(rows));
|
|
161
|
+
const hasDCOFile = yield* fs.exists(DCO_FILE_PATH);
|
|
162
|
+
blocks.push(hasDCOFile ? Report.ok(`DCO file: ${DCO_FILE_PATH}`) : Report.skip("No DCO file (signoff not required)"));
|
|
180
163
|
const releaseFormat = yield* detectReleaseFormat;
|
|
181
|
-
yield* Output.detail(`Release format: ${releaseFormat}`);
|
|
182
164
|
const scopes = yield* Effect.catch(Commitlint.detectScopes, () => Effect.succeed([]));
|
|
183
165
|
const scopeDisplay = scopes.length > 0 ? scopes.join(", ") : "(none - not a monorepo or no packages found)";
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
166
|
+
blocks.push(Report.line("Detected settings"), Report.detail(`DCO required: ${Commitlint.detectDCO()}`), Report.detail(`Release format: ${releaseFormat}`), Report.detail(`Detected scopes: ${scopeDisplay}`));
|
|
167
|
+
const sectionsHealthy = rows.every(([, , state]) => state === "up-to-date");
|
|
168
|
+
const hasIssues = !foundConfig || !hasHuskyHook || !sectionsHealthy;
|
|
169
|
+
blocks.push(hasIssues ? Report.fail("Commitlint needs configuration") : Report.ok("Commitlint is configured correctly"));
|
|
170
|
+
return {
|
|
171
|
+
title: "commitlint",
|
|
172
|
+
blocks,
|
|
173
|
+
verdict: hasIssues ? "failure" : "success",
|
|
174
|
+
fixedByInit: hasIssues
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Check the commitlint setup, print its report on stdout, and set exit code 1
|
|
180
|
+
* when it found a misconfiguration (see {@link commitCheckSection}).
|
|
181
|
+
*
|
|
182
|
+
* @returns An Effect that performs validation and prints its report
|
|
183
|
+
*
|
|
184
|
+
* @internal
|
|
185
|
+
*/
|
|
186
|
+
function runCommitCheck() {
|
|
187
|
+
return Effect.gen(function* () {
|
|
188
|
+
const section = yield* commitCheckSection();
|
|
189
|
+
yield* Report.print([Report.heading(section.title), ...section.blocks]);
|
|
190
|
+
if (section.verdict === "failure") yield* CliExit.set(1);
|
|
188
191
|
});
|
|
189
192
|
}
|
|
190
193
|
|
|
191
194
|
//#endregion
|
|
192
|
-
export { runCommitCheck };
|
|
195
|
+
export { commitCheckSection, runCommitCheck };
|
package/commands/commit/init.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Report } from "../../internal/report.js";
|
|
2
|
+
import { CommandError } from "../../internal/command-error.js";
|
|
2
3
|
import { HUSKY_HOOK_PATH, POST_CHECKOUT_HOOK_PATH, POST_COMMIT_HOOK_PATH, POST_MERGE_HOOK_PATH } from "./constants.js";
|
|
3
4
|
import { dirname } from "node:path";
|
|
4
5
|
import { SavvyBaseSection, SavvyHooksSection, SavvyToolchainSection, savvyBasePreamble, savvyHooksHygiene, savvyInstallBlock, savvyToolSection, savvyToolchainCheck } from "@savvy-web/silk-effects";
|
|
@@ -70,7 +71,18 @@ export default CommitlintConfig.silk();
|
|
|
70
71
|
function makeExecutable(path) {
|
|
71
72
|
return Effect.tryPromise({
|
|
72
73
|
try: () => chmod(path, EXECUTABLE_MODE),
|
|
73
|
-
catch: (e) =>
|
|
74
|
+
catch: (e) => CommandError.from(e, {
|
|
75
|
+
message: `could not make ${path} executable`,
|
|
76
|
+
hint: `Check that you own ${path}, then re-run the init.`
|
|
77
|
+
})
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** The failure for a `--config` path given as absolute. */
|
|
81
|
+
function absoluteConfigError(config) {
|
|
82
|
+
return new CommandError({
|
|
83
|
+
message: "Config path must be relative to repository root, not absolute",
|
|
84
|
+
detail: [`got ${config}`],
|
|
85
|
+
hint: "Pass the config path relative to the repository root, e.g. lib/configs/<name>.config.ts."
|
|
74
86
|
});
|
|
75
87
|
}
|
|
76
88
|
/**
|
|
@@ -89,14 +101,14 @@ function runCommitInit(opts) {
|
|
|
89
101
|
return Effect.gen(function* () {
|
|
90
102
|
const fs = yield* FileSystem.FileSystem;
|
|
91
103
|
const ms = yield* ManagedSection;
|
|
92
|
-
if (config.startsWith("/")) yield* Effect.fail(
|
|
93
|
-
yield*
|
|
104
|
+
if (config.startsWith("/")) return yield* Effect.fail(absoluteConfigError(config));
|
|
105
|
+
yield* Report.print([Report.heading("commitlint")]);
|
|
94
106
|
yield* fs.makeDirectory(".husky", { recursive: true });
|
|
95
107
|
if (force) yield* fs.writeFileString(HUSKY_HOOK_PATH, COMMIT_MSG_HEADER);
|
|
96
108
|
else yield* ensureHookFile(HUSKY_HOOK_PATH, COMMIT_MSG_HEADER);
|
|
97
109
|
const commitResults = yield* ms.syncAll(HUSKY_HOOK_PATH, [SavvyBaseSection.section(savvyBasePreamble()), savvyCommitBlock(config)]);
|
|
98
110
|
yield* makeExecutable(HUSKY_HOOK_PATH);
|
|
99
|
-
yield*
|
|
111
|
+
yield* Report.print([Report.ok(`${force ? "Replaced" : "Synced"} ${HUSKY_HOOK_PATH} (${commitResults.map((r) => r._tag).join(", ")})`)]);
|
|
100
112
|
for (const [hookPath, installHook] of HYGIENE_HOOKS) {
|
|
101
113
|
yield* ensureHookFile(hookPath, HYGIENE_HEADER);
|
|
102
114
|
const sections = [SavvyHooksSection.section(savvyHooksHygiene())];
|
|
@@ -106,17 +118,16 @@ function runCommitInit(opts) {
|
|
|
106
118
|
}
|
|
107
119
|
yield* ms.syncAll(hookPath, sections);
|
|
108
120
|
yield* makeExecutable(hookPath);
|
|
109
|
-
yield*
|
|
121
|
+
yield* Report.print([Report.ok(`Synced ${hookPath}`)]);
|
|
110
122
|
}
|
|
111
|
-
if ((yield* fs.exists(config)) && !force) yield*
|
|
123
|
+
if ((yield* fs.exists(config)) && !force) yield* Report.print([Report.warn(`${config} already exists (use --force to overwrite)`)]);
|
|
112
124
|
else {
|
|
113
125
|
const configDir = dirname(config);
|
|
114
126
|
if (configDir && configDir !== ".") yield* fs.makeDirectory(configDir, { recursive: true });
|
|
115
127
|
yield* fs.writeFileString(config, CONFIG_CONTENT);
|
|
116
|
-
yield*
|
|
128
|
+
yield* Report.print([Report.ok(`Created ${config}`)]);
|
|
117
129
|
}
|
|
118
|
-
yield*
|
|
119
|
-
yield* Output.ok("Install @commitlint/cli if it is not already installed");
|
|
130
|
+
yield* Report.print([Report.line(""), Report.ok("Install @commitlint/cli if it is not already installed")]);
|
|
120
131
|
});
|
|
121
132
|
}
|
|
122
133
|
|
package/commands/commit/lint.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Report } from "../../internal/report.js";
|
|
2
2
|
import { buildCommitlintInvocation } from "./commitlint-invocation.js";
|
|
3
3
|
import { Argument, CliError, Command } from "effect/cli";
|
|
4
4
|
import { Commitlint } from "@savvy-web/silk-effects";
|
|
@@ -76,7 +76,7 @@ function runCommitLint(file) {
|
|
|
76
76
|
if (stdout.length > 0) yield* Effect.sync(() => process.stdout.write(stdout));
|
|
77
77
|
if (stderr.length > 0) yield* Effect.sync(() => process.stderr.write(stderr));
|
|
78
78
|
if (!passed) return yield* Effect.fail(new CliError.UserError({ cause: `Commit message at ${file} failed commitlint validation` }));
|
|
79
|
-
yield*
|
|
79
|
+
yield* Report.print([Report.ok("Commit message passes the Silk commitlint preset")]);
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
82
|
/* v8 ignore next */
|