@savvy-web/cli 2.10.0 → 2.11.1

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 CHANGED
@@ -76,7 +76,7 @@ const rootCommand = Command.make("savvy").pipe(Command.withSubcommands([
76
76
  * CLI application: reads argv from the Stdio service provided by NodeServices.
77
77
  * (v4's `Command.run` takes only `version` — the name comes from the root command.)
78
78
  */
79
- const cli = Command.run(rootCommand, { version: "2.10.0" });
79
+ const cli = Command.run(rootCommand, { version: "2.11.1" });
80
80
  /**
81
81
  * Shared workspace services from `@effected/workspaces`, wired as a
82
82
  * self-contained unit and built ONCE (layers memoize by reference).
@@ -2,7 +2,7 @@ import { HUSKY_HOOK_PATH, POST_CHECKOUT_HOOK_PATH, POST_COMMIT_HOOK_PATH, POST_M
2
2
  import { SECTION_DEF, savvyCommitBlock } from "./init.js";
3
3
  import { CheckOutcome, ManagedSection } from "@effected/templates";
4
4
  import { VersioningStrategy } from "@effected/workspaces";
5
- import { ChangesetConfigReader, Commitlint, SavvyBaseSection, SavvyHooksSection, SavvyToolchainSection, savvyBasePreamble, savvyHooksHygiene, savvyToolchainCheck } from "@savvy-web/silk-effects";
5
+ import { ChangesetConfigReader, Commitlint, SavvyBaseSection, SavvyHooksSection, SavvyToolchainSection, savvyBasePreamble, savvyHooksHygiene, savvyInstallBlock, savvyToolchainCheck } from "@savvy-web/silk-effects";
6
6
  import { Effect, FileSystem, Option } from "effect";
7
7
 
8
8
  //#region src/commands/commit/check.ts
@@ -155,6 +155,16 @@ function runCommitCheck() {
155
155
  yield* Effect.log(`${BULLET} Hygiene hook: ${hookPath} section not found (run 'savvy init' to add)`);
156
156
  }
157
157
  if (hookPath === ".husky/post-commit") continue;
158
+ const installHook = hookPath === ".husky/post-checkout" ? "post-checkout" : "post-merge";
159
+ const installStatus = yield* ms.check(hookPath, savvyInstallBlock(installHook));
160
+ if (CheckOutcome.$is("UpToDate")(installStatus)) yield* Effect.log(`${"✓"} Dependency install: ${hookPath}`);
161
+ else if (CheckOutcome.$is("Drifted")(installStatus)) {
162
+ sectionsHealthy = false;
163
+ yield* Effect.log(`${"⚠"} Dependency install: ${hookPath} outdated (run 'savvy init' to update)`);
164
+ } else {
165
+ sectionsHealthy = false;
166
+ yield* Effect.log(`${BULLET} Dependency install: ${hookPath} section not found (run 'savvy init' to add)`);
167
+ }
158
168
  const toolchainStatus = yield* ms.check(hookPath, SavvyToolchainSection.section(savvyToolchainCheck()));
159
169
  if (CheckOutcome.$is("UpToDate")(toolchainStatus)) yield* Effect.log(`${"✓"} Toolchain check: ${hookPath}`);
160
170
  else if (CheckOutcome.$is("Drifted")(toolchainStatus)) {
@@ -1,6 +1,6 @@
1
1
  import { HUSKY_HOOK_PATH, POST_CHECKOUT_HOOK_PATH, POST_COMMIT_HOOK_PATH, POST_MERGE_HOOK_PATH } from "./constants.js";
2
2
  import { CommentStyle, ManagedSection, SectionId } from "@effected/templates";
3
- import { SavvyBaseSection, SavvyHooksSection, SavvyToolchainSection, savvyBasePreamble, savvyHooksHygiene, savvyToolSection, savvyToolchainCheck } from "@savvy-web/silk-effects";
3
+ import { SavvyBaseSection, SavvyHooksSection, SavvyToolchainSection, savvyBasePreamble, savvyHooksHygiene, savvyInstallBlock, savvyToolSection, savvyToolchainCheck } from "@savvy-web/silk-effects";
4
4
  import { Effect, FileSystem } from "effect";
5
5
  import { dirname } from "node:path";
6
6
  import { chmod } from "node:fs/promises";
@@ -20,6 +20,17 @@ const SECTION_DEF = SectionId.make({
20
20
  });
21
21
  /** Header written when creating a fresh commit-msg hook. */
22
22
  const COMMIT_MSG_HEADER = "#!/usr/bin/env sh\n# Commit-msg hook with savvy managed sections\n# Custom hooks can go above, below, or between the managed sections\n\n";
23
+ /**
24
+ * The hygiene hooks, each paired with the install variant it should carry.
25
+ *
26
+ * `undefined` means the hook gets hygiene only — post-commit fires on every
27
+ * commit, where neither a drift warning nor an install is worth the noise.
28
+ */
29
+ const HYGIENE_HOOKS = [
30
+ [POST_CHECKOUT_HOOK_PATH, "post-checkout"],
31
+ [POST_MERGE_HOOK_PATH, "post-merge"],
32
+ [POST_COMMIT_HOOK_PATH, void 0]
33
+ ];
23
34
  /** Header written when creating a fresh hygiene hook (post-checkout / post-merge / post-commit). */
24
35
  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";
25
36
  /**
@@ -85,14 +96,13 @@ function runCommitInit(opts) {
85
96
  const commitResults = yield* ms.syncAll(HUSKY_HOOK_PATH, [SavvyBaseSection.section(savvyBasePreamble()), savvyCommitBlock(config)]);
86
97
  yield* makeExecutable(HUSKY_HOOK_PATH);
87
98
  yield* Effect.log(`${"✓"} ${force ? "Replaced" : "Synced"} ${HUSKY_HOOK_PATH} (${commitResults.map((r) => r._tag).join(", ")})`);
88
- for (const hookPath of [
89
- POST_CHECKOUT_HOOK_PATH,
90
- POST_MERGE_HOOK_PATH,
91
- POST_COMMIT_HOOK_PATH
92
- ]) {
99
+ for (const [hookPath, installHook] of HYGIENE_HOOKS) {
93
100
  yield* ensureHookFile(hookPath, HYGIENE_HEADER);
94
101
  const sections = [SavvyHooksSection.section(savvyHooksHygiene())];
95
- if (hookPath !== ".husky/post-commit") sections.push(SavvyToolchainSection.section(savvyToolchainCheck()));
102
+ if (installHook !== void 0) {
103
+ sections.push(SavvyToolchainSection.section(savvyToolchainCheck()));
104
+ sections.push(savvyInstallBlock(installHook));
105
+ }
96
106
  yield* ms.syncAll(hookPath, sections);
97
107
  yield* makeExecutable(hookPath);
98
108
  yield* Effect.log(`${"✓"} Synced ${hookPath}`);
@@ -1,7 +1,7 @@
1
1
  import { BIOME_VERSION } from "./biome-version.js";
2
2
  import { Tool, ToolDiscovery } from "@effected/commands";
3
3
  import { CheckOutcome, ManagedSection } from "@effected/templates";
4
- import { ConfigDiscovery, Lint, SavvyBaseSection, SavvyHooksSection, SavvyToolchainSection, savvyBasePreamble, savvyHooksHygiene, savvyToolchainCheck } from "@savvy-web/silk-effects";
4
+ import { ConfigDiscovery, Lint, SavvyBaseSection, SavvyHooksSection, SavvyToolchainSection, savvyBasePreamble, savvyHooksHygiene, savvyInstallBlock, savvyToolchainCheck } from "@savvy-web/silk-effects";
5
5
  import { Effect, FileSystem, Option } from "effect";
6
6
  import { Jsonc } from "@effected/jsonc";
7
7
  import { isDeepStrictEqual } from "node:util";
@@ -197,6 +197,7 @@ function runLintCheck(opts) {
197
197
  ];
198
198
  const shellHookStatuses = [];
199
199
  const toolchainStatuses = [];
200
+ const installStatuses = [];
200
201
  for (const hookPath of shellHookPaths) {
201
202
  if (!(yield* fs.exists(hookPath))) {
202
203
  shellHookStatuses.push({
@@ -222,6 +223,22 @@ function runLintCheck(opts) {
222
223
  warnings.push(`${WARNING} ${hookPath} savvy-hooks section is outdated.\n Run 'savvy init' to update.`);
223
224
  }
224
225
  if (hookPath === Lint.POST_COMMIT_HOOK_PATH) continue;
226
+ const installHook = hookPath === Lint.POST_CHECKOUT_HOOK_PATH ? "post-checkout" : "post-merge";
227
+ const installResult = yield* ms.check(hookPath, savvyInstallBlock(installHook));
228
+ const installFound = !CheckOutcome.$is("Absent")(installResult);
229
+ const installUpToDate = CheckOutcome.$is("UpToDate")(installResult);
230
+ installStatuses.push({
231
+ path: hookPath,
232
+ found: installFound,
233
+ isUpToDate: installUpToDate
234
+ });
235
+ if (!installFound) {
236
+ sectionsHealthy = false;
237
+ warnings.push(`${WARNING} ${hookPath} has no savvy-install section.\n Run 'savvy init' to add it.`);
238
+ } else if (!installUpToDate) {
239
+ sectionsHealthy = false;
240
+ warnings.push(`${WARNING} ${hookPath} savvy-install section is outdated.\n Run 'savvy init' to update.`);
241
+ }
225
242
  const toolchainResult = yield* ms.check(hookPath, SavvyToolchainSection.section(savvyToolchainCheck()));
226
243
  const toolchainFound = !CheckOutcome.$is("Absent")(toolchainResult);
227
244
  const toolchainUpToDate = CheckOutcome.$is("UpToDate")(toolchainResult);
@@ -276,6 +293,9 @@ function runLintCheck(opts) {
276
293
  for (const status of shellHookStatuses) if (!status.found) yield* Effect.log(`${BULLET} ${status.path}: savvy-hooks section not found`);
277
294
  else if (status.isUpToDate) yield* Effect.log(`${CHECK_MARK} ${status.path}: up-to-date`);
278
295
  else yield* Effect.log(`${WARNING} ${status.path}: outdated (run 'savvy init' to update)`);
296
+ for (const status of installStatuses) if (!status.found) yield* Effect.log(`${BULLET} ${status.path}: savvy-install section not found`);
297
+ else if (status.isUpToDate) yield* Effect.log(`${CHECK_MARK} ${status.path}: savvy-install up-to-date`);
298
+ else yield* Effect.log(`${WARNING} ${status.path}: savvy-install outdated (run 'savvy init' to update)`);
279
299
  for (const status of toolchainStatuses) if (!status.found) yield* Effect.log(`${BULLET} ${status.path}: savvy-toolchain section not found`);
280
300
  else if (status.isUpToDate) yield* Effect.log(`${CHECK_MARK} ${status.path}: savvy-toolchain up-to-date`);
281
301
  else yield* Effect.log(`${WARNING} ${status.path}: savvy-toolchain outdated (run 'savvy init' to update)`);
@@ -1,9 +1,9 @@
1
1
  import { BIOME_VERSION } from "./biome-version.js";
2
2
  import { ManagedSection } from "@effected/templates";
3
3
  import { WorkspaceDiscovery } from "@effected/workspaces";
4
- import { BiomeSchemaSync, Lint, SavvyBaseSection, SavvyHooksSection, SavvyToolchainSection, savvyBasePreamble, savvyHooksHygiene, savvyToolchainCheck } from "@savvy-web/silk-effects";
4
+ import { BiomeSchemaSync, LIFECYCLE_SCRIPTS_CONFIG_KEY, Lint, SavvyBaseSection, SavvyHooksSection, SavvyToolchainSection, publishesBuiltLinkDirectory, savvyBasePreamble, savvyHooksHygiene, savvyInstallBlock, savvyToolchainCheck } from "@savvy-web/silk-effects";
5
5
  import { Effect, FileSystem } from "effect";
6
- import { dirname } from "node:path";
6
+ import { dirname, join } from "node:path";
7
7
  import { Jsonc, JsoncEdit, JsoncModifier } from "@effected/jsonc";
8
8
  import { chmod } from "node:fs/promises";
9
9
  import { isDeepStrictEqual } from "node:util";
@@ -27,6 +27,17 @@ const JSONC_FORMAT = {
27
27
  };
28
28
  /** Header written when creating a fresh pre-commit hook. */
29
29
  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";
30
+ /**
31
+ * The hygiene hooks, each paired with the install variant it should carry.
32
+ *
33
+ * `undefined` means the hook gets hygiene only — post-commit fires on every
34
+ * commit, where neither a drift warning nor an install is worth the noise.
35
+ */
36
+ const HYGIENE_HOOKS = [
37
+ [Lint.POST_CHECKOUT_HOOK_PATH, "post-checkout"],
38
+ [Lint.POST_MERGE_HOOK_PATH, "post-merge"],
39
+ [Lint.POST_COMMIT_HOOK_PATH, void 0]
40
+ ];
30
41
  /** Header written when creating a fresh hygiene hook (post-checkout / post-merge / post-commit). */
31
42
  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";
32
43
  /**
@@ -148,6 +159,38 @@ function biomeConfigRoots() {
148
159
  });
149
160
  }
150
161
  /**
162
+ * Point out that this workspace needs lifecycle scripts, and how to allow them.
163
+ *
164
+ * @remarks
165
+ * The `savvy-install` hook block skips lifecycle scripts unless the LOCAL git
166
+ * config key authorizes them. A workspace publishing through built link
167
+ * directories cannot survive that default — its own workspace links resolve
168
+ * through directories a `prepare` script produces — so it has to be told.
169
+ *
170
+ * This reports and does not set. Enabling scripts means every later hook-time
171
+ * install may execute code from whatever revision was just checked out, which is
172
+ * a trust decision belonging to the person who owns the checkout, not to a
173
+ * command they ran to write some config files. Setting it for them silently
174
+ * would give back most of what reading the flag from local config bought.
175
+ *
176
+ * Every failure reads as "say nothing": an unreadable or malformed manifest
177
+ * cannot make the notice wrong, only absent.
178
+ */
179
+ function noticeLifecycleScripts() {
180
+ return Effect.gen(function* () {
181
+ const fs = yield* FileSystem.FileSystem;
182
+ const roots = yield* biomeConfigRoots();
183
+ for (const root of roots) {
184
+ const manifest = yield* fs.readFileString(join(root, "package.json")).pipe(Effect.map((raw) => JSON.parse(raw)), Effect.catch(() => Effect.succeed(void 0)));
185
+ if (publishesBuiltLinkDirectory(manifest)) {
186
+ yield* Effect.log(`${WARNING} This workspace publishes through built link directories, so a hook-time install must run lifecycle scripts.`);
187
+ yield* Effect.log(` Allow them with: git config --local ${LIFECYCLE_SCRIPTS_CONFIG_KEY} true`);
188
+ return;
189
+ }
190
+ }
191
+ });
192
+ }
193
+ /**
151
194
  * Find and sync biome config `$schema` URLs to match the pinned {@link BIOME_VERSION}.
152
195
  *
153
196
  * @remarks
@@ -214,19 +257,19 @@ function runLintInit(opts) {
214
257
  const preCommitResults = yield* ms.syncAll(Lint.HUSKY_HOOK_PATH, [SavvyBaseSection.section(savvyBasePreamble()), Lint.savvyLintBlock(config)]);
215
258
  yield* makeExecutable(Lint.HUSKY_HOOK_PATH);
216
259
  yield* Effect.log(`${CHECK_MARK} ${force ? "Replaced" : "Synced"} ${Lint.HUSKY_HOOK_PATH} (${preCommitResults.map((r) => r._tag).join(", ")})`);
217
- if (presetIncludesShellScripts(preset)) for (const hookPath of [
218
- Lint.POST_CHECKOUT_HOOK_PATH,
219
- Lint.POST_MERGE_HOOK_PATH,
220
- Lint.POST_COMMIT_HOOK_PATH
221
- ]) {
260
+ if (presetIncludesShellScripts(preset)) for (const [hookPath, installHook] of HYGIENE_HOOKS) {
222
261
  yield* ensureHookFile(hookPath, HYGIENE_HEADER);
223
262
  yield* ms.remove(hookPath, Lint.LegacySavvyLintHygieneDef);
224
263
  const sections = [SavvyHooksSection.section(savvyHooksHygiene())];
225
- if (hookPath !== Lint.POST_COMMIT_HOOK_PATH) sections.push(SavvyToolchainSection.section(savvyToolchainCheck()));
264
+ if (installHook !== void 0) {
265
+ sections.push(SavvyToolchainSection.section(savvyToolchainCheck()));
266
+ sections.push(savvyInstallBlock(installHook));
267
+ }
226
268
  yield* ms.syncAll(hookPath, sections);
227
269
  yield* makeExecutable(hookPath);
228
270
  yield* Effect.log(`${CHECK_MARK} Synced ${hookPath}`);
229
271
  }
272
+ if (presetIncludesShellScripts(preset)) yield* noticeLifecycleScripts();
230
273
  if (presetIncludesMarkdown(preset)) yield* writeMarkdownlintConfig(fs, preset, force);
231
274
  yield* syncBiomeSchemas();
232
275
  if ((yield* fs.exists(config)) && !force) yield* Effect.log(`${WARNING} ${config} already exists (use --force to overwrite)`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/cli",
3
- "version": "2.10.0",
3
+ "version": "2.11.1",
4
4
  "private": false,
5
5
  "description": "The savvy CLI — unified commit, changeset, and lint commands for the Silk Suite",
6
6
  "homepage": "https://github.com/savvy-web/systems/tree/main/packages/cli",
@@ -31,14 +31,14 @@
31
31
  "savvy": "bin/savvy.js"
32
32
  },
33
33
  "dependencies": {
34
- "@effect/platform-node": "4.0.0-rc.109",
35
- "@effected/commands": "^0.5.0",
36
- "@effected/git": "^0.10.0",
37
- "@effected/jsonc": "^0.8.1",
38
- "@effected/templates": "^0.4.0",
39
- "@effected/workspaces": "^0.19.0",
40
- "@effected/yaml": "^0.12.0",
41
- "@savvy-web/silk-effects": "7.4.0",
42
- "effect": "4.0.0-rc.109"
34
+ "@effect/platform-node": "4.0.0-rc.112",
35
+ "@effected/commands": "^0.6.1",
36
+ "@effected/git": "^0.12.0",
37
+ "@effected/jsonc": "^0.9.0",
38
+ "@effected/templates": "^0.5.0",
39
+ "@effected/workspaces": "^0.20.1",
40
+ "@effected/yaml": "^0.14.0",
41
+ "@savvy-web/silk-effects": "7.5.1",
42
+ "effect": "4.0.0-rc.112"
43
43
  }
44
44
  }