@savvy-web/cli 0.3.0 → 0.4.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/bin/savvy.d.ts +1 -0
- package/bin/savvy.js +17 -1
- package/cli/index.js +123 -0
- package/commands/changeset/commands/analyze-branch.js +108 -0
- package/commands/changeset/commands/check.js +71 -0
- package/commands/changeset/commands/classify.js +69 -0
- package/commands/changeset/commands/config-show.js +100 -0
- package/commands/changeset/commands/config-validate.js +63 -0
- package/commands/changeset/commands/deps-detect.js +103 -0
- package/commands/changeset/commands/deps-regen.js +277 -0
- package/commands/changeset/commands/init.js +634 -0
- package/commands/changeset/commands/lint.js +62 -0
- package/commands/changeset/commands/release-surface.js +96 -0
- package/commands/changeset/commands/transform.js +88 -0
- package/commands/changeset/commands/validate-file.js +52 -0
- package/commands/changeset/commands/version.js +178 -0
- package/commands/changeset/index.js +42 -0
- package/commands/changeset/utils/config-gate.js +59 -0
- package/commands/check.js +74 -0
- package/commands/clean.js +186 -0
- package/commands/commit/check.js +170 -0
- package/commands/commit/constants.js +10 -0
- package/commands/commit/hook.js +22 -0
- package/commands/commit/hooks/post-commit-verify.js +121 -0
- package/commands/commit/hooks/pre-commit-message.js +64 -0
- package/commands/commit/hooks/session-start.js +69 -0
- package/commands/commit/hooks/user-prompt-submit.js +42 -0
- package/commands/commit/index.js +20 -0
- package/commands/commit/init.js +127 -0
- package/commands/init.js +88 -0
- package/commands/lint/check.js +306 -0
- package/commands/lint/fmt.js +64 -0
- package/commands/lint/index.js +20 -0
- package/commands/lint/init.js +221 -0
- package/index.d.ts +237 -244
- package/index.js +14 -1
- package/package.json +39 -51
- package/841.js +0 -2394
- package/tsdoc-metadata.json +0 -11
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { Command, Options } from "@effect/cli";
|
|
2
|
+
import { BiomeSchemaSync, Lint, ManagedSection, SavvyBaseSection, SavvyHooksSection, savvyBasePreamble, savvyHooksHygiene } from "@savvy-web/silk-effects";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
import { applyEdits, modify, parse } from "jsonc-effect";
|
|
6
|
+
import { FileSystem } from "@effect/platform";
|
|
7
|
+
import { chmod } from "node:fs/promises";
|
|
8
|
+
import { isDeepStrictEqual } from "node:util";
|
|
9
|
+
|
|
10
|
+
//#region src/commands/lint/init.ts
|
|
11
|
+
/**
|
|
12
|
+
* Init command - bootstrap lint-staged configuration.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
/** Unicode checkmark symbol. */
|
|
17
|
+
const CHECK_MARK = "✓";
|
|
18
|
+
/** Unicode warning symbol. */
|
|
19
|
+
const WARNING = "⚠";
|
|
20
|
+
/** Executable file permission mode. */
|
|
21
|
+
const EXECUTABLE_MODE = 493;
|
|
22
|
+
/** Formatting options for jsonc-effect surgical edits. */
|
|
23
|
+
const JSONC_FORMAT = {
|
|
24
|
+
tabSize: 1,
|
|
25
|
+
insertSpaces: false
|
|
26
|
+
};
|
|
27
|
+
/** Header written when creating a fresh pre-commit hook. */
|
|
28
|
+
const PRE_COMMIT_HEADER = "#!/usr/bin/env sh\n# Pre-commit hook with savvy managed sections\n# Custom hooks can go above, below, or between the managed sections\n\n";
|
|
29
|
+
/** Header written when creating a fresh hygiene hook (post-checkout / post-merge). */
|
|
30
|
+
const HYGIENE_HEADER = "#!/usr/bin/env sh\n# Managed by savvy-hooks\n# Custom hooks can go above or below the managed section\n\n";
|
|
31
|
+
/**
|
|
32
|
+
* Check if a preset includes the ShellScripts handler.
|
|
33
|
+
*
|
|
34
|
+
* @param preset - The preset to check
|
|
35
|
+
* @returns True if the preset includes ShellScripts (standard and silk)
|
|
36
|
+
*/
|
|
37
|
+
function presetIncludesShellScripts(preset) {
|
|
38
|
+
return preset !== "minimal";
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Check if a preset includes markdown tooling.
|
|
42
|
+
*
|
|
43
|
+
* @param preset - The preset to check
|
|
44
|
+
* @returns True if the preset includes Markdown (standard and silk)
|
|
45
|
+
*/
|
|
46
|
+
function presetIncludesMarkdown(preset) {
|
|
47
|
+
return preset !== "minimal";
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Generate the lint-staged config file content.
|
|
51
|
+
*
|
|
52
|
+
* @param preset - The preset to use
|
|
53
|
+
* @returns Config file content
|
|
54
|
+
*/
|
|
55
|
+
function generateConfigContent(preset) {
|
|
56
|
+
return `/**
|
|
57
|
+
* lint-staged configuration
|
|
58
|
+
* Generated by savvy lint init
|
|
59
|
+
*/
|
|
60
|
+
import { Preset } from "@savvy-web/silk/lint";
|
|
61
|
+
|
|
62
|
+
export default Preset.${preset}();
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Write or update the markdownlint-cli2 config file.
|
|
67
|
+
*
|
|
68
|
+
* @param fs - FileSystem service
|
|
69
|
+
* @param preset - The active preset
|
|
70
|
+
* @param force - Whether to overwrite the entire file
|
|
71
|
+
* @returns Effect that manages the markdownlint config
|
|
72
|
+
*/
|
|
73
|
+
function writeMarkdownlintConfig(fs, preset, force) {
|
|
74
|
+
return Effect.gen(function* () {
|
|
75
|
+
const configExists = yield* fs.exists(Lint.MARKDOWNLINT_CONFIG_PATH);
|
|
76
|
+
const fullTemplate = JSON.stringify(Lint.MARKDOWNLINT_TEMPLATE, null, " ");
|
|
77
|
+
if (!configExists) {
|
|
78
|
+
yield* fs.makeDirectory("lib/configs", { recursive: true });
|
|
79
|
+
yield* fs.writeFileString(Lint.MARKDOWNLINT_CONFIG_PATH, `${fullTemplate}\n`);
|
|
80
|
+
yield* Effect.log(`${CHECK_MARK} Created ${Lint.MARKDOWNLINT_CONFIG_PATH}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (preset !== "silk") {
|
|
84
|
+
yield* Effect.log(`${CHECK_MARK} ${Lint.MARKDOWNLINT_CONFIG_PATH}: exists (not managed by ${preset} preset)`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (force) {
|
|
88
|
+
yield* fs.writeFileString(Lint.MARKDOWNLINT_CONFIG_PATH, `${fullTemplate}\n`);
|
|
89
|
+
yield* Effect.log(`${CHECK_MARK} Replaced ${Lint.MARKDOWNLINT_CONFIG_PATH} (--force)`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const existingText = yield* fs.readFileString(Lint.MARKDOWNLINT_CONFIG_PATH);
|
|
93
|
+
const existingParsed = yield* parse(existingText);
|
|
94
|
+
let updatedText = existingText;
|
|
95
|
+
let schemaUpdated = false;
|
|
96
|
+
if (existingParsed.$schema !== Lint.MARKDOWNLINT_SCHEMA) {
|
|
97
|
+
const edits = yield* modify(updatedText, ["$schema"], Lint.MARKDOWNLINT_SCHEMA, { formattingOptions: JSONC_FORMAT });
|
|
98
|
+
updatedText = yield* applyEdits(updatedText, edits);
|
|
99
|
+
schemaUpdated = true;
|
|
100
|
+
}
|
|
101
|
+
const existingConfig = existingParsed.config;
|
|
102
|
+
if (!(existingConfig !== void 0 && isDeepStrictEqual(existingConfig, Lint.MARKDOWNLINT_CONFIG))) {
|
|
103
|
+
yield* Effect.log(`${WARNING} ${Lint.MARKDOWNLINT_CONFIG_PATH}: config rules differ from template (use --force to overwrite)`);
|
|
104
|
+
if (schemaUpdated) {
|
|
105
|
+
yield* fs.writeFileString(Lint.MARKDOWNLINT_CONFIG_PATH, updatedText);
|
|
106
|
+
yield* Effect.log(`${CHECK_MARK} Updated $schema in ${Lint.MARKDOWNLINT_CONFIG_PATH}`);
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (schemaUpdated) {
|
|
111
|
+
yield* fs.writeFileString(Lint.MARKDOWNLINT_CONFIG_PATH, updatedText);
|
|
112
|
+
yield* Effect.log(`${CHECK_MARK} Updated $schema in ${Lint.MARKDOWNLINT_CONFIG_PATH}`);
|
|
113
|
+
} else yield* Effect.log(`${CHECK_MARK} ${Lint.MARKDOWNLINT_CONFIG_PATH}: up-to-date`);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Find and sync biome config `$schema` URLs to match the peer dependency version.
|
|
118
|
+
*
|
|
119
|
+
* @returns Effect that syncs biome schemas and logs results
|
|
120
|
+
*/
|
|
121
|
+
function syncBiomeSchemas() {
|
|
122
|
+
return Effect.gen(function* () {
|
|
123
|
+
const version = process.env.__BIOME_PEER_VERSION__;
|
|
124
|
+
if (!version) return;
|
|
125
|
+
const result = yield* (yield* BiomeSchemaSync).sync(version);
|
|
126
|
+
for (const configPath of result.current) yield* Effect.log(`${CHECK_MARK} ${configPath}: biome $schema up-to-date`);
|
|
127
|
+
for (const configPath of result.updated) yield* Effect.log(`${CHECK_MARK} Updated $schema in ${configPath}`);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/* v8 ignore start -- CLI option definitions; handler tested individually */
|
|
131
|
+
const forceOption = Options.boolean("force").pipe(Options.withAlias("f"), Options.withDescription("Overwrite the pre-commit hook and config file entirely (managed sections in post-checkout/post-merge are never force-reset)"), Options.withDefault(false));
|
|
132
|
+
const configOption = Options.text("config").pipe(Options.withAlias("c"), Options.withDescription("Relative path for the lint-staged config file (from repo root)"), Options.withDefault(Lint.DEFAULT_CONFIG_PATH));
|
|
133
|
+
const presetOption = Options.choice("preset", [
|
|
134
|
+
"minimal",
|
|
135
|
+
"standard",
|
|
136
|
+
"silk"
|
|
137
|
+
]).pipe(Options.withAlias("p"), Options.withDescription("Preset to use: minimal, standard, or silk"), Options.withDefault("silk"));
|
|
138
|
+
/* v8 ignore stop */
|
|
139
|
+
/** Make a file executable. */
|
|
140
|
+
function makeExecutable(path) {
|
|
141
|
+
return Effect.tryPromise({
|
|
142
|
+
try: () => chmod(path, EXECUTABLE_MODE),
|
|
143
|
+
catch: (e) => new Error(String(e))
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/** Ensure a hook file exists, writing `header` if it does not. */
|
|
147
|
+
function ensureHookFile(path, header) {
|
|
148
|
+
return Effect.gen(function* () {
|
|
149
|
+
const fs = yield* FileSystem.FileSystem;
|
|
150
|
+
if (!(yield* fs.exists(path))) yield* fs.writeFileString(path, header);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Run the full lint init pipeline.
|
|
155
|
+
*
|
|
156
|
+
* Exported so Task B5's unified `savvy init` orchestrator can invoke the
|
|
157
|
+
* lint-staged init step directly without going through the CLI command layer.
|
|
158
|
+
*
|
|
159
|
+
* @param opts - The same options the CLI command receives
|
|
160
|
+
* @returns An Effect that performs initialization
|
|
161
|
+
*
|
|
162
|
+
* @internal
|
|
163
|
+
*/
|
|
164
|
+
function runLintInit(opts) {
|
|
165
|
+
const { force, config, preset } = opts;
|
|
166
|
+
return Effect.gen(function* () {
|
|
167
|
+
const fs = yield* FileSystem.FileSystem;
|
|
168
|
+
const ms = yield* ManagedSection;
|
|
169
|
+
if (config.startsWith("/")) yield* Effect.fail(/* @__PURE__ */ new Error("Config path must be relative to repository root, not absolute"));
|
|
170
|
+
yield* Effect.log("Initializing lint-staged configuration...\n");
|
|
171
|
+
yield* fs.makeDirectory(".husky", { recursive: true });
|
|
172
|
+
if (force) yield* fs.writeFileString(Lint.HUSKY_HOOK_PATH, PRE_COMMIT_HEADER);
|
|
173
|
+
else yield* ensureHookFile(Lint.HUSKY_HOOK_PATH, PRE_COMMIT_HEADER);
|
|
174
|
+
const preCommitResults = yield* ms.syncMany(Lint.HUSKY_HOOK_PATH, [SavvyBaseSection.block(savvyBasePreamble()), Lint.savvyLintBlock(config)]);
|
|
175
|
+
yield* makeExecutable(Lint.HUSKY_HOOK_PATH);
|
|
176
|
+
yield* Effect.log(`${CHECK_MARK} ${force ? "Replaced" : "Synced"} ${Lint.HUSKY_HOOK_PATH} (${preCommitResults.map((r) => r._tag).join(", ")})`);
|
|
177
|
+
if (presetIncludesShellScripts(preset)) for (const hookPath of [Lint.POST_CHECKOUT_HOOK_PATH, Lint.POST_MERGE_HOOK_PATH]) {
|
|
178
|
+
yield* ensureHookFile(hookPath, HYGIENE_HEADER);
|
|
179
|
+
yield* ms.remove(hookPath, Lint.LegacySavvyLintHygieneDef);
|
|
180
|
+
yield* ms.sync(hookPath, SavvyHooksSection.block(savvyHooksHygiene()));
|
|
181
|
+
yield* makeExecutable(hookPath);
|
|
182
|
+
yield* Effect.log(`${CHECK_MARK} Synced ${hookPath}`);
|
|
183
|
+
}
|
|
184
|
+
if (presetIncludesMarkdown(preset)) yield* writeMarkdownlintConfig(fs, preset, force);
|
|
185
|
+
yield* syncBiomeSchemas().pipe(Effect.catchTag("BiomeSyncError", (e) => Effect.log(`${WARNING} Could not sync biome $schema: ${e.message}`)));
|
|
186
|
+
if ((yield* fs.exists(config)) && !force) yield* Effect.log(`${WARNING} ${config} already exists (use --force to overwrite)`);
|
|
187
|
+
else {
|
|
188
|
+
const configDir = dirname(config);
|
|
189
|
+
if (configDir && configDir !== ".") yield* fs.makeDirectory(configDir, { recursive: true });
|
|
190
|
+
yield* fs.writeFileString(config, generateConfigContent(preset));
|
|
191
|
+
yield* Effect.log(`${CHECK_MARK} Created ${config} (preset: ${preset})`);
|
|
192
|
+
}
|
|
193
|
+
yield* Effect.log("\nDone! Lint-staged is ready to use.");
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Init command implementation.
|
|
198
|
+
*
|
|
199
|
+
* @remarks
|
|
200
|
+
* Writes:
|
|
201
|
+
* - `.husky/pre-commit` — `savvy-base` preamble + `savvy-lint` tool section, in order
|
|
202
|
+
* (via `ManagedSection.syncMany`).
|
|
203
|
+
* - `.husky/post-checkout` and `.husky/post-merge` — co-owned `savvy-hooks` hygiene
|
|
204
|
+
* (idempotent, shared with `@savvy-web/commitlint`). Migrates legacy `SAVVY-LINT`
|
|
205
|
+
* hygiene blocks by removing them before writing the new section.
|
|
206
|
+
* - `lib/configs/.markdownlint-cli2.jsonc` config (when preset includes Markdown).
|
|
207
|
+
* - lint-staged config at the specified path.
|
|
208
|
+
*
|
|
209
|
+
* Users may add custom commands above, below, or between the managed sections.
|
|
210
|
+
* `--force` resets only the pre-commit hook and the config file; the hygiene sections
|
|
211
|
+
* are always reconciled with `sync`.
|
|
212
|
+
*/
|
|
213
|
+
/* v8 ignore next 3 -- CLI registration; handler tested via runLintInit */
|
|
214
|
+
const initCommand = Command.make("init", {
|
|
215
|
+
force: forceOption,
|
|
216
|
+
config: configOption,
|
|
217
|
+
preset: presetOption
|
|
218
|
+
}, (opts) => runLintInit(opts)).pipe(Command.withDescription("Initialize lint-staged configuration and husky hooks"));
|
|
219
|
+
|
|
220
|
+
//#endregion
|
|
221
|
+
export { runLintInit };
|