@savvy-web/silk-effects 3.3.0 → 4.0.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.
Files changed (72) hide show
  1. package/changesets/changelog/getReleaseLine.js +1 -1
  2. package/changesets/constants.js +0 -18
  3. package/changesets/index.js +10 -24
  4. package/changesets/schemas/changeset.js +8 -4
  5. package/changesets/schemas/dependency-table.js +15 -4
  6. package/changesets/schemas/git.js +7 -2
  7. package/changesets/schemas/github.js +4 -4
  8. package/changesets/schemas/options.js +3 -3
  9. package/changesets/schemas/package-scope.js +2 -5
  10. package/changesets/schemas/primitives.js +2 -2
  11. package/changesets/schemas/release-plan.js +12 -8
  12. package/changesets/schemas/version-files.js +4 -4
  13. package/changesets/services/branch-analyzer.js +60 -191
  14. package/changesets/services/changelog.js +2 -15
  15. package/changesets/services/config-inspector.js +109 -75
  16. package/changesets/services/deps-regen.js +58 -32
  17. package/changesets/services/github.js +3 -16
  18. package/changesets/services/maintenance-reason.js +5 -1
  19. package/changesets/services/markdown.js +3 -16
  20. package/changesets/services/release-planner.js +4 -13
  21. package/changesets/utils/commit-parser.js +0 -18
  22. package/changesets/utils/dep-diff.js +0 -27
  23. package/changesets/utils/git.js +15 -50
  24. package/changesets/utils/issue-refs.js +0 -13
  25. package/changesets/utils/logger.js +0 -13
  26. package/changesets/utils/markdown-link.js +0 -14
  27. package/changesets/utils/publishability.js +11 -24
  28. package/changesets/utils/strip-frontmatter.js +0 -19
  29. package/changesets/utils/version-files.js +85 -51
  30. package/commitlint/config/schema.js +9 -5
  31. package/commitlint/detection/scopes.js +2 -7
  32. package/commitlint/formatter/messages.js +0 -5
  33. package/commitlint/hook/diagnostics/branch.js +12 -12
  34. package/commitlint/hook/diagnostics/open-issues.js +13 -7
  35. package/commitlint/hook/diagnostics/signing.js +30 -37
  36. package/commitlint/hook/envelope.js +2 -8
  37. package/commitlint/hook/silence-logger.js +14 -12
  38. package/index.d.ts +899 -934
  39. package/lint/cli/templates/markdownlint.gen.js +0 -9
  40. package/lint/handlers/PnpmWorkspace.js +26 -12
  41. package/lint/utils/Filter.js +0 -13
  42. package/lint/utils/Workspace.js +10 -6
  43. package/package.json +11 -10
  44. package/repos/index.js +3 -5
  45. package/repos/schemas/manifest.js +7 -13
  46. package/repos/schemas/reports.js +5 -1
  47. package/repos/services/config-store.js +6 -10
  48. package/repos/services/manager.js +52 -112
  49. package/schemas/CommentStyle.js +1 -1
  50. package/schemas/ResolvedTool.js +52 -17
  51. package/schemas/SectionBlock.js +1 -1
  52. package/schemas/SectionDefinition.js +2 -2
  53. package/schemas/TagStrategySchemas.js +1 -1
  54. package/schemas/ToolDefinition.js +1 -1
  55. package/schemas/ToolResults.js +1 -1
  56. package/schemas/VersioningSchemas.js +28 -9
  57. package/schemas/WorkspaceAnalysisSchemas.js +27 -17
  58. package/services/BiomeSchemaSync.js +7 -8
  59. package/services/ChangesetConfig.js +2 -2
  60. package/services/ChangesetConfigReader.js +7 -8
  61. package/services/ConfigDiscovery.js +5 -6
  62. package/services/ManagedSection.js +3 -4
  63. package/services/SilkPublishability.js +38 -15
  64. package/services/SilkWorkspaceAnalyzer.js +7 -9
  65. package/services/TagStrategy.js +3 -3
  66. package/services/ToolDiscovery.js +22 -21
  67. package/services/VersioningStrategy.js +2 -2
  68. package/tsdoc-metadata.json +1 -1
  69. package/turbo/schemas/DryRun.js +8 -14
  70. package/turbo/schemas/results.js +8 -8
  71. package/turbo/services/TurboInspector.js +30 -23
  72. package/utils/ToolCommand.js +38 -12
@@ -1,10 +1,11 @@
1
1
  import { LegacyVersionFilesSchema } from "../schemas/version-files.js";
2
2
  import { jsonPathGet, jsonPathResolve, parseJsonPath } from "./jsonpath.js";
3
- import { Effect, Schema } from "effect";
3
+ import { Effect, Option, Path, Schema } from "effect";
4
4
  import { readFileSync, writeFileSync } from "node:fs";
5
5
  import { join, relative, resolve } from "node:path";
6
- import { globSync } from "tinyglobby";
7
- import { applyEdits, modify, parse } from "jsonc-effect";
6
+ import { GlobPattern } from "@effected/glob";
7
+ import { descend } from "@effected/walker";
8
+ import { Jsonc, JsoncEdit, JsoncFormattingOptions, JsoncModifier } from "@effected/jsonc";
8
9
 
9
10
  //#region src/changesets/utils/version-files.ts
10
11
  /**
@@ -42,13 +43,14 @@ import { applyEdits, modify, parse } from "jsonc-effect";
42
43
  *
43
44
  * @example
44
45
  * ```typescript
46
+ * import { Effect } from "effect";
45
47
  * import { VersionFiles } from "../utils/version-files.js";
46
48
  *
47
49
  * const configs = VersionFiles.extractVersionFiles(parsedConfig);
48
50
  * if (configs) {
49
- * const updates = VersionFiles.processVersionFiles("/path/to/project", configs);
51
+ * const updates = yield* VersionFiles.processVersionFiles("/path/to/project", configs);
50
52
  * for (const update of updates) {
51
- * console.log(`Updated ${update.filePath} to ${update.version}`);
53
+ * yield* Effect.log(`Updated ${update.filePath} to ${update.version}`);
52
54
  * }
53
55
  * }
54
56
  * ```
@@ -159,25 +161,24 @@ var VersionFiles = class VersionFiles {
159
161
  * Resolve glob patterns to absolute file paths.
160
162
  *
161
163
  * @remarks
162
- * Uses `tinyglobby` to expand each config's glob pattern relative to
163
- * `cwd`. The `node_modules` directory is always ignored. Returns
164
- * tuples pairing each resolved absolute path with its originating config.
164
+ * Compiles each config's glob with `@effected/glob` and expands it against
165
+ * a filesystem walk rooted at `cwd` via `@effected/walker`'s `descend`
166
+ * (literal patterns fast-path a direct stat; magic patterns walk from
167
+ * their literal prefix). The `node_modules` and `.git` directories are
168
+ * always ignored. Returns tuples pairing each resolved absolute path with
169
+ * its originating config, per-glob results sorted by relative path.
165
170
  *
166
171
  * @param configs - Version file configurations
167
172
  * @param cwd - Project root directory
168
- * @returns Array of `[filePath, config]` tuples
173
+ * @returns Effect of `[filePath, config]` tuples
169
174
  */
170
175
  static resolveGlobs(configs, cwd) {
171
- const results = [];
172
- const resolvedCwd = resolve(cwd);
173
- for (const config of configs) {
174
- const matches = globSync(config.glob, {
175
- cwd: resolvedCwd,
176
- ignore: ["**/node_modules/**"]
177
- });
178
- for (const match of matches) results.push([join(resolvedCwd, match), config]);
179
- }
180
- return results;
176
+ return Effect.gen(function* () {
177
+ const results = [];
178
+ const resolvedCwd = resolve(cwd);
179
+ for (const config of configs) for (const match of yield* expandGlob(config.glob, resolvedCwd)) results.push([join(resolvedCwd, match), config]);
180
+ return results;
181
+ });
181
182
  }
182
183
  /**
183
184
  * Detect indentation from file content.
@@ -252,7 +253,7 @@ var VersionFiles = class VersionFiles {
252
253
  */
253
254
  static computeUpdate(original, jsonPaths, version) {
254
255
  let content = original;
255
- const obj = Effect.runSync(parse(content));
256
+ const obj = Effect.runSync(Jsonc.parse(content));
256
257
  const previousValues = jsonPaths.flatMap((jp) => jsonPathGet(obj, jp));
257
258
  const indent = VersionFiles.detectIndent(content);
258
259
  const eol = content.includes("\r\n") ? "\r\n" : "\n";
@@ -286,9 +287,9 @@ var VersionFiles = class VersionFiles {
286
287
  * the updated document, or `undefined` when nothing changed.
287
288
  *
288
289
  * @remarks
289
- * Delegates to `jsonc-effect`'s {@link modify} + {@link applyEdits}
290
- * (requires `jsonc-effect >= 0.3.1`, whose edit spans touch only the target
291
- * value), so every other byte of the document is preserved. When the leaf
290
+ * Delegates to `@effected/jsonc`'s `JsoncModifier.modify` +
291
+ * `JsoncEdit.applyAll` (whose edit spans touch only the target value), so
292
+ * every other byte of the document is preserved. When the leaf
292
293
  * of a wildcard-free path does not exist, `modify` inserts the property
293
294
  * after the last sibling using the supplied formatting options — the only
294
295
  * case where the detected indent matters. A path whose parent is missing or
@@ -304,11 +305,11 @@ var VersionFiles = class VersionFiles {
304
305
  */
305
306
  static applyVersionEdit(content, concretePath, version, indentUnit, eol) {
306
307
  const insertSpaces = !indentUnit.includes(" ");
307
- const program = modify(content, [...concretePath], version, { formattingOptions: {
308
+ const program = JsoncModifier.modify(content, [...concretePath], version, { formattingOptions: JsoncFormattingOptions.make({
308
309
  insertSpaces,
309
310
  tabSize: insertSpaces ? indentUnit.length : 1,
310
311
  eol
311
- } }).pipe(Effect.flatMap((edits) => applyEdits(content, edits)), Effect.map((updated) => updated === content ? void 0 : updated), Effect.catchTag("JsoncModificationError", () => Effect.succeed(void 0)));
312
+ }) }).pipe(Effect.map((edits) => JsoncEdit.applyAll(content, edits)), Effect.map((updated) => updated === content ? void 0 : updated), Effect.catchTag("JsoncModificationError", () => Effect.succeed(void 0)));
312
313
  return Effect.runSync(program);
313
314
  }
314
315
  /**
@@ -323,36 +324,39 @@ var VersionFiles = class VersionFiles {
323
324
  * @param cwd - Project root directory
324
325
  * @param configs - Validated version file configurations
325
326
  * @param dryRun - If true, do not write files (default: `false`)
326
- * @returns Array of update results
327
- * @throws If any file cannot be read or parsed
327
+ * @returns Effect of update results; a file that cannot be read or parsed
328
+ * is a defect (the legacy path treats it as a caller bug, as the
329
+ * previous synchronous `throw` did)
328
330
  */
329
331
  static processVersionFiles(cwd, configs, dryRun = false, packages = []) {
330
- const workspaces = VersionFiles.discoverVersions(cwd, packages);
331
- const rootVersion = workspaces.find((ws) => ws.path === resolve(cwd))?.version ?? "0.0.0";
332
- const resolved = VersionFiles.resolveGlobs(configs, cwd);
333
- const updates = [];
334
- for (const [filePath, config] of resolved) {
335
- const jsonPaths = config.paths ?? ["$.version"];
336
- const version = config.package ? workspaces.find((ws) => ws.name === config.package)?.version ?? rootVersion : VersionFiles.resolveVersion(filePath, workspaces, rootVersion);
337
- try {
338
- if (dryRun) {
339
- const content = readFileSync(filePath, "utf-8");
340
- const { previousValues, totalChanged } = VersionFiles.computeUpdate(content, jsonPaths, version);
341
- if (totalChanged > 0) updates.push({
342
- filePath,
343
- jsonPaths,
344
- version,
345
- previousValues
346
- });
347
- } else {
348
- const result = VersionFiles.updateFile(filePath, jsonPaths, version);
349
- if (result) updates.push(result);
332
+ return Effect.gen(function* () {
333
+ const workspaces = VersionFiles.discoverVersions(cwd, packages);
334
+ const rootVersion = workspaces.find((ws) => ws.path === resolve(cwd))?.version ?? "0.0.0";
335
+ const resolved = yield* VersionFiles.resolveGlobs(configs, cwd);
336
+ const updates = [];
337
+ for (const [filePath, config] of resolved) {
338
+ const jsonPaths = config.paths ?? ["$.version"];
339
+ const version = config.package ? workspaces.find((ws) => ws.name === config.package)?.version ?? rootVersion : VersionFiles.resolveVersion(filePath, workspaces, rootVersion);
340
+ try {
341
+ if (dryRun) {
342
+ const content = readFileSync(filePath, "utf-8");
343
+ const { previousValues, totalChanged } = VersionFiles.computeUpdate(content, jsonPaths, version);
344
+ if (totalChanged > 0) updates.push({
345
+ filePath,
346
+ jsonPaths,
347
+ version,
348
+ previousValues
349
+ });
350
+ } else {
351
+ const result = VersionFiles.updateFile(filePath, jsonPaths, version);
352
+ if (result) updates.push(result);
353
+ }
354
+ } catch (error) {
355
+ throw new Error(`Failed to update ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
350
356
  }
351
- } catch (error) {
352
- throw new Error(`Failed to update ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
353
357
  }
354
- }
355
- return updates;
358
+ return updates;
359
+ });
356
360
  }
357
361
  /**
358
362
  * Apply version-file updates from the resolved (post-`ConfigInspector`)
@@ -401,6 +405,36 @@ var VersionFiles = class VersionFiles {
401
405
  }
402
406
  };
403
407
  /**
408
+ * Expand a glob pattern against the filesystem, returning matching FILE paths
409
+ * relative to `cwd` (POSIX separators), sorted by relative path.
410
+ *
411
+ * @remarks
412
+ * The walk is `@effected/walker`'s `descend` (literal fast path,
413
+ * `enumerationPrefix` bounding, `node_modules`/`.git` pruning), with
414
+ * `onUnreadable: "skip"` preserving this module's silent-skip policy for
415
+ * unreadable directories. Matching semantics are minimatch DEFAULTS via the
416
+ * compiled pattern — notably, wildcards do not match dotfiles, same as the
417
+ * previous `tinyglobby` defaults. An uncompilable pattern expands to no
418
+ * matches. `descend`'s `Path` requirement is satisfied internally with the
419
+ * core POSIX `Path.layer` — this module already speaks POSIX relative match
420
+ * paths and node-bound absolute paths throughout.
421
+ *
422
+ * @param source - The glob pattern, repo-relative
423
+ * @param cwd - Absolute directory the pattern is resolved against
424
+ * @returns Effect of relative paths of matching files, sorted
425
+ *
426
+ * @internal
427
+ */
428
+ function expandGlob(source, cwd) {
429
+ return Effect.option(GlobPattern.compile(source)).pipe(Effect.flatMap(Option.match({
430
+ onNone: () => Effect.succeed([]),
431
+ onSome: (pattern) => descend(pattern, {
432
+ cwd,
433
+ onUnreadable: "skip"
434
+ })
435
+ })), Effect.provide(Path.layer));
436
+ }
437
+ /**
404
438
  * Read the `version` field from a `package.json` in the given directory.
405
439
  *
406
440
  * @param dir - Absolute path to the directory containing `package.json`
@@ -1,4 +1,4 @@
1
- import { Schema } from "effect";
1
+ import { Effect, Schema } from "effect";
2
2
 
3
3
  //#region src/commitlint/config/schema.ts
4
4
  /**
@@ -11,7 +11,11 @@ import { Schema } from "effect";
11
11
  *
12
12
  * @internal
13
13
  */
14
- const ReleaseFormatSchema = Schema.Literal("semver", "packages", "scoped");
14
+ const ReleaseFormatSchema = Schema.Literals([
15
+ "semver",
16
+ "packages",
17
+ "scoped"
18
+ ]);
15
19
  /**
16
20
  * Effect Schema for validating {@link ConfigOptions}.
17
21
  *
@@ -26,9 +30,9 @@ const ConfigOptionsSchema = Schema.Struct({
26
30
  scopes: Schema.optional(Schema.Array(Schema.String)),
27
31
  additionalScopes: Schema.optional(Schema.Array(Schema.String)),
28
32
  releaseFormat: Schema.optional(ReleaseFormatSchema),
29
- emojis: Schema.optionalWith(Schema.Boolean, { default: () => false }),
30
- bodyMaxLineLength: Schema.optionalWith(Schema.Number.pipe(Schema.positive()), { default: () => 300 }),
31
- noMarkdown: Schema.optionalWith(Schema.Boolean, { default: () => true }),
33
+ emojis: Schema.Boolean.pipe(Schema.withDecodingDefaultType(Effect.succeed(false))),
34
+ bodyMaxLineLength: Schema.Number.check(Schema.isGreaterThan(0)).pipe(Schema.withDecodingDefaultType(Effect.succeed(300))),
35
+ noMarkdown: Schema.Boolean.pipe(Schema.withDecodingDefaultType(Effect.succeed(true))),
32
36
  cwd: Schema.optional(Schema.String)
33
37
  });
34
38
  /**
@@ -1,13 +1,8 @@
1
1
  import { Effect } from "effect";
2
- import { WorkspaceDiscovery } from "workspaces-effect";
2
+ import { WorkspaceDiscovery } from "@effected/workspaces";
3
3
 
4
4
  //#region src/commitlint/detection/scopes.ts
5
5
  /**
6
- * Workspace scope detection module.
7
- *
8
- * @internal
9
- */
10
- /**
11
6
  * Extract scope-friendly name from a package name.
12
7
  *
13
8
  * @param name - Package name (e.g., `@scope/package-name` or `package-name`)
@@ -23,7 +18,7 @@ function extractScopeName(name) {
23
18
  * Detect package scopes from workspace configuration.
24
19
  *
25
20
  * @remarks
26
- * Uses workspaces-effect to find all packages in the workspace and extracts
21
+ * Uses `@effected/workspaces` to find all packages in the workspace and extracts
27
22
  * their names as potential commit scopes. For scoped packages like
28
23
  * `@scope/package-name`, only the package name portion is used as the scope.
29
24
  *
@@ -1,10 +1,5 @@
1
1
  //#region src/commitlint/formatter/messages.ts
2
2
  /**
3
- * Error message templates for the formatter.
4
- *
5
- * @internal
6
- */
7
- /**
8
3
  * Detailed error explanations for common rule failures.
9
4
  *
10
5
  * @remarks
@@ -1,30 +1,30 @@
1
- import { Effect } from "effect";
2
- import { execFile } from "node:child_process";
3
- import { promisify } from "node:util";
1
+ import { Effect, Option } from "effect";
2
+ import { Git } from "@effected/git";
4
3
 
5
4
  //#region src/commitlint/hook/diagnostics/branch.ts
6
5
  /**
7
6
  * Branch and inferred-ticket detection.
8
7
  *
8
+ * @remarks
9
+ * Branch lookup runs through `@effected/git`'s `currentBranch` (Option-shaped:
10
+ * a detached `HEAD` is `Option.none`, not the literal string `"HEAD"`). Any
11
+ * git failure degrades to the `{ branch: null }` fallback, preserving the
12
+ * never-fails contract of the v3 implementation.
13
+ *
9
14
  * @internal
10
15
  */
11
- const execFileP = promisify(execFile);
12
16
  const TICKET_RE = /^[a-z]+\/(\d+)[-/_]/;
13
17
  function inferTicketId(branch) {
14
18
  const m = branch.match(TICKET_RE);
15
19
  return m ? Number(m[1]) : null;
16
20
  }
17
21
  function readBranchInfo() {
18
- return Effect.tryPromise(async () => {
19
- const { stdout } = await execFileP("git", [
20
- "rev-parse",
21
- "--abbrev-ref",
22
- "HEAD"
23
- ]);
24
- const branch = stdout.trim();
22
+ return Effect.gen(function* () {
23
+ const branchOption = yield* (yield* Git).currentBranch(process.cwd());
24
+ const branch = Option.getOrNull(branchOption);
25
25
  return {
26
26
  branch,
27
- inferredTicketId: inferTicketId(branch)
27
+ inferredTicketId: branch === null ? null : inferTicketId(branch)
28
28
  };
29
29
  }).pipe(Effect.orElseSucceed(() => ({
30
30
  branch: null,
@@ -1,15 +1,20 @@
1
1
  import { readCache, writeCache } from "./cache.js";
2
2
  import { Effect } from "effect";
3
- import { execFile } from "node:child_process";
4
- import { promisify } from "node:util";
3
+ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
5
4
 
6
5
  //#region src/commitlint/hook/diagnostics/open-issues.ts
7
6
  /**
8
7
  * Open-issue lookup via gh CLI, cached on disk.
9
8
  *
9
+ * @remarks
10
+ * The `gh` invocations are not git, so they stay hand-rolled — the spawn
11
+ * mechanism moved from promisified `node:child_process.execFile` onto
12
+ * `effect/unstable/process` `ChildProcess`. Any failure (gh missing, not
13
+ * logged in, no repo, malformed JSON) degrades to `null`, preserving the
14
+ * never-fails contract of the v3 implementation.
15
+ *
10
16
  * @internal
11
17
  */
12
- const execFileP = promisify(execFile);
13
18
  const ISSUES_CACHE_TTL_SECONDS = 600;
14
19
  /** Relative path under CLAUDE_PROJECT_DIR where the open-issues cache lives. */
15
20
  const ISSUES_CACHE_RELATIVE_PATH = ".claude/cache/issues.json";
@@ -18,16 +23,17 @@ function readOpenIssuesFromCache(cachePath, ttlSeconds = 600) {
18
23
  }
19
24
  function fetchAndCacheOpenIssues(cachePath) {
20
25
  return Effect.gen(function* () {
21
- const repo = (yield* Effect.tryPromise(() => execFileP("gh", [
26
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
27
+ const repo = (yield* spawner.string(ChildProcess.make("gh", [
22
28
  "repo",
23
29
  "view",
24
30
  "--json",
25
31
  "nameWithOwner",
26
32
  "-q",
27
33
  ".nameWithOwner"
28
- ]))).stdout.trim();
34
+ ]))).trim();
29
35
  if (!repo) return null;
30
- const listResult = yield* Effect.tryPromise(() => execFileP("gh", [
36
+ const listStdout = yield* spawner.string(ChildProcess.make("gh", [
31
37
  "issue",
32
38
  "list",
33
39
  "--repo",
@@ -39,7 +45,7 @@ function fetchAndCacheOpenIssues(cachePath) {
39
45
  "--json",
40
46
  "number,title"
41
47
  ]));
42
- const parsed = JSON.parse(listResult.stdout);
48
+ const parsed = yield* Effect.try(() => JSON.parse(listStdout));
43
49
  yield* writeCache(cachePath, parsed);
44
50
  return parsed;
45
51
  }).pipe(Effect.orElseSucceed(() => null));
@@ -1,15 +1,23 @@
1
- import { Effect } from "effect";
2
- import { execFile } from "node:child_process";
3
- import { promisify } from "node:util";
1
+ import { Effect, Option } from "effect";
2
+ import { Git } from "@effected/git";
4
3
  import { stat } from "node:fs/promises";
4
+ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
5
5
 
6
6
  //#region src/commitlint/hook/diagnostics/signing.ts
7
7
  /**
8
8
  * GPG / SSH signing diagnostic.
9
9
  *
10
+ * @remarks
11
+ * The `git config` reads run through `@effected/git`'s `configGet`
12
+ * (Option-shaped: an unset key is `Option.none`). The `gpg` /
13
+ * `gpg-connect-agent` probes are not git, so they stay hand-rolled on
14
+ * `effect/unstable/process` `ChildProcess`; every probe degrades to its v3
15
+ * fallback value (`keyResolves: false`, `agentResponsive: false`) on any
16
+ * failure, and the diagnostic as a whole degrades to
17
+ * {@link FALLBACK_DIAGNOSTIC}, preserving the never-fails contract.
18
+ *
10
19
  * @internal
11
20
  */
12
- const execFileP = promisify(execFile);
13
21
  function parseGpgKeyExpiry(colonsOutput) {
14
22
  for (const line of colonsOutput.split("\n")) {
15
23
  if (!line.startsWith("sec:")) continue;
@@ -38,19 +46,6 @@ function buildSigningDiagnostic(raw) {
38
46
  warnings
39
47
  };
40
48
  }
41
- async function gitConfig(key) {
42
- try {
43
- const { stdout } = await execFileP("git", [
44
- "config",
45
- "--get",
46
- key
47
- ]);
48
- const v = stdout.trim();
49
- return v.length > 0 ? v : null;
50
- } catch {
51
- return null;
52
- }
53
- }
54
49
  const FALLBACK_DIAGNOSTIC = {
55
50
  format: "none",
56
51
  autoSignEnabled: false,
@@ -59,38 +54,36 @@ const FALLBACK_DIAGNOSTIC = {
59
54
  agentResponsive: false,
60
55
  warnings: ["signing diagnostic unavailable"]
61
56
  };
57
+ /** `git config --get <key>`, degraded to null when unset or unreadable. */
58
+ const gitConfig = (key) => Effect.gen(function* () {
59
+ const value = yield* (yield* Git).configGet(process.cwd(), key);
60
+ return Option.getOrNull(value);
61
+ }).pipe(Effect.orElseSucceed(() => null));
62
62
  function readSigningDiagnostic() {
63
- return Effect.tryPromise(async () => {
64
- const gpgFormat = await gitConfig("gpg.format");
65
- const commitGpgsign = await gitConfig("commit.gpgsign");
66
- const signingKey = await gitConfig("user.signingkey");
67
- const sshAllowedSignersFile = await gitConfig("gpg.ssh.allowedSignersFile");
63
+ return Effect.gen(function* () {
64
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
65
+ const gpgFormat = yield* gitConfig("gpg.format");
66
+ const commitGpgsign = yield* gitConfig("commit.gpgsign");
67
+ const signingKey = yield* gitConfig("user.signingkey");
68
+ const sshAllowedSignersFile = yield* gitConfig("gpg.ssh.allowedSignersFile");
68
69
  const isSsh = gpgFormat === "ssh";
69
70
  let keyResolves = false;
70
71
  let keyExpiry = null;
71
- if (signingKey) if (isSsh) try {
72
+ if (signingKey) if (isSsh) keyResolves = yield* Effect.tryPromise(async () => {
72
73
  await stat(signingKey);
73
- keyResolves = true;
74
- } catch {
75
- keyResolves = false;
76
- }
77
- else try {
78
- const { stdout } = await execFileP("gpg", [
74
+ return true;
75
+ }).pipe(Effect.orElseSucceed(() => false));
76
+ else {
77
+ const stdout = yield* spawner.string(ChildProcess.make("gpg", [
79
78
  "--list-secret-keys",
80
79
  "--with-colons",
81
80
  signingKey
82
- ]);
81
+ ])).pipe(Effect.orElseSucceed(() => ""));
83
82
  keyResolves = stdout.trim().length > 0;
84
83
  keyExpiry = parseGpgKeyExpiry(stdout);
85
- } catch {
86
- keyResolves = false;
87
84
  }
88
85
  let agentResponsive = true;
89
- if (!isSsh) try {
90
- await execFileP("gpg-connect-agent", ["/bye"], { timeout: 1e3 });
91
- } catch {
92
- agentResponsive = false;
93
- }
86
+ if (!isSsh) agentResponsive = yield* spawner.exitCode(ChildProcess.make("gpg-connect-agent", ["/bye"])).pipe(Effect.timeout("1 second"), Effect.map((code) => code === 0), Effect.orElseSucceed(() => false));
94
87
  return buildSigningDiagnostic({
95
88
  gpgFormat,
96
89
  commitGpgsign,
@@ -14,18 +14,12 @@ import { Schema } from "effect";
14
14
  const PreToolUseEnvelope = Schema.Struct({
15
15
  hook_event_name: Schema.Literal("PreToolUse"),
16
16
  tool_name: Schema.String,
17
- tool_input: Schema.Record({
18
- key: Schema.String,
19
- value: Schema.Unknown
20
- })
17
+ tool_input: Schema.Record(Schema.String, Schema.Unknown)
21
18
  });
22
19
  const PostToolUseEnvelope = Schema.Struct({
23
20
  hook_event_name: Schema.Literal("PostToolUse"),
24
21
  tool_name: Schema.String,
25
- tool_input: Schema.Record({
26
- key: Schema.String,
27
- value: Schema.Unknown
28
- }),
22
+ tool_input: Schema.Record(Schema.String, Schema.Unknown),
29
23
  tool_response: Schema.Struct({
30
24
  interrupted: Schema.optional(Schema.Boolean),
31
25
  exit_code: Schema.optional(Schema.Number),
@@ -1,4 +1,4 @@
1
- import { Layer, LogLevel, Logger } from "effect";
1
+ import { Layer, Logger, References } from "effect";
2
2
 
3
3
  //#region src/commitlint/hook/silence-logger.ts
4
4
  /**
@@ -7,13 +7,13 @@ import { Layer, LogLevel, Logger } from "effect";
7
7
  * @remarks
8
8
  * Hook subcommands reserve stdout for the JSON envelope they emit to Claude
9
9
  * Code. Effect's default logger writes messages to stdout via `console.log`,
10
- * which pollutes that contract (e.g., workspaces-effect emits "Workspace root
11
- * found" on every CLI invocation). This Layer combines two protections:
10
+ * which pollutes that contract (e.g., workspace discovery may log on every
11
+ * CLI invocation). This Layer combines two protections:
12
12
  *
13
- * 1. {@link Logger.minimumLogLevel} at `Warning` silences routine Info/Debug
13
+ * 1. `References.MinimumLogLevel` at `Warn` silences routine Info/Debug
14
14
  * traffic so the common case never reaches the logger at all.
15
- * 2. A default-logger replacement that routes anything which *does* fire to
16
- * stderr, so even Warning/Error output cannot corrupt the stdout envelope.
15
+ * 2. A logger-set replacement that routes anything which *does* fire to
16
+ * stderr, so even Warn/Error output cannot corrupt the stdout envelope.
17
17
  *
18
18
  * Only the hook command wrappers provide this Layer, so regular `savvy`
19
19
  * commands keep their stdout output intact.
@@ -21,16 +21,18 @@ import { Layer, LogLevel, Logger } from "effect";
21
21
  * @internal
22
22
  */
23
23
  /**
24
- * Replacement for the default logger that writes every message to stderr,
25
- * leaving stdout reserved for the hook's JSON envelope.
24
+ * Replacement logger that writes every message to stderr, leaving stdout
25
+ * reserved for the hook's JSON envelope. `Logger.layer` without
26
+ * `mergeWithExisting` REPLACES the current logger set, so the default
27
+ * stdout logger never fires.
26
28
  */
27
- const StderrLogger = Logger.replace(Logger.defaultLogger, Logger.make(({ message }) => {
29
+ const StderrLogger = Logger.layer([Logger.make(({ message }) => {
28
30
  process.stderr.write(`${JSON.stringify(message)}\n`);
29
- }));
31
+ })]);
30
32
  /** Minimum log level layer that suppresses Info and below. */
31
- const MinLogLevel = Logger.minimumLogLevel(LogLevel.Warning);
33
+ const MinLogLevel = Layer.succeed(References.MinimumLogLevel, "Warn");
32
34
  /**
33
- * Combined hook logger layer: stderr redirect merged with the Warning minimum
35
+ * Combined hook logger layer: stderr redirect merged with the Warn minimum
34
36
  * log level. Provided by hook command wrappers to keep stdout corruption-proof.
35
37
  */
36
38
  const HookSilencer = Layer.merge(StderrLogger, MinLogLevel);