@savvy-web/silk-effects 0.6.1 → 1.0.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.
Files changed (155) hide show
  1. package/README.md +48 -17
  2. package/_virtual/_rolldown/runtime.js +18 -0
  3. package/changesets/api/categories.js +247 -0
  4. package/changesets/api/changelog.js +134 -0
  5. package/changesets/api/dependency-table.js +163 -0
  6. package/changesets/api/linter.js +168 -0
  7. package/changesets/api/transformer.js +140 -0
  8. package/changesets/categories/index.js +299 -0
  9. package/changesets/categories/types.js +66 -0
  10. package/changesets/changelog/formatting.js +119 -0
  11. package/changesets/changelog/getDependencyReleaseLine.js +114 -0
  12. package/changesets/changelog/getReleaseLine.js +122 -0
  13. package/changesets/changelog/index.js +99 -0
  14. package/changesets/constants.js +43 -0
  15. package/changesets/errors.js +305 -0
  16. package/changesets/index.js +146 -0
  17. package/changesets/markdownlint/index.js +29 -0
  18. package/changesets/markdownlint/rules/content-structure.js +98 -0
  19. package/changesets/markdownlint/rules/dependency-table-format.js +170 -0
  20. package/changesets/markdownlint/rules/heading-hierarchy.js +61 -0
  21. package/changesets/markdownlint/rules/required-sections.js +54 -0
  22. package/changesets/markdownlint/rules/uncategorized-content.js +54 -0
  23. package/changesets/markdownlint/rules/utils.js +30 -0
  24. package/changesets/remark/plugins/aggregate-dependency-tables.js +47 -0
  25. package/changesets/remark/plugins/contributor-footnotes.js +123 -0
  26. package/changesets/remark/plugins/deduplicate-items.js +30 -0
  27. package/changesets/remark/plugins/issue-link-refs.js +58 -0
  28. package/changesets/remark/plugins/merge-sections.js +43 -0
  29. package/changesets/remark/plugins/normalize-format.js +47 -0
  30. package/changesets/remark/plugins/reorder-sections.js +34 -0
  31. package/changesets/remark/presets.js +119 -0
  32. package/changesets/remark/rules/content-structure.js +22 -0
  33. package/changesets/remark/rules/dependency-table-format.js +40 -0
  34. package/changesets/remark/rules/heading-hierarchy.js +19 -0
  35. package/changesets/remark/rules/required-sections.js +17 -0
  36. package/changesets/remark/rules/uncategorized-content.js +31 -0
  37. package/changesets/schemas/changeset.js +146 -0
  38. package/changesets/schemas/dependency-table.js +189 -0
  39. package/changesets/schemas/git.js +69 -0
  40. package/changesets/schemas/github.js +175 -0
  41. package/changesets/schemas/options.js +182 -0
  42. package/changesets/schemas/package-scope.js +128 -0
  43. package/changesets/schemas/primitives.js +72 -0
  44. package/changesets/schemas/version-files.js +151 -0
  45. package/changesets/services/branch-analyzer.js +278 -0
  46. package/changesets/services/changelog.js +50 -0
  47. package/changesets/services/config-inspector.js +390 -0
  48. package/changesets/services/github.js +178 -0
  49. package/changesets/services/markdown.js +106 -0
  50. package/changesets/services/workspace-snapshot.js +182 -0
  51. package/changesets/utils/commit-parser.js +80 -0
  52. package/changesets/utils/dep-diff.js +77 -0
  53. package/changesets/utils/dependency-table.js +347 -0
  54. package/changesets/utils/issue-refs.js +101 -0
  55. package/changesets/utils/jsonpath.js +175 -0
  56. package/changesets/utils/logger.js +50 -0
  57. package/changesets/utils/markdown-link.js +57 -0
  58. package/changesets/utils/publishability.js +39 -0
  59. package/changesets/utils/remark-pipeline.js +79 -0
  60. package/changesets/utils/section-parser.js +94 -0
  61. package/changesets/utils/strip-frontmatter.js +46 -0
  62. package/changesets/utils/version-blocks.js +108 -0
  63. package/changesets/utils/version-files.js +336 -0
  64. package/changesets/utils/worktree-snapshot.js +142 -0
  65. package/changesets/vendor/github-info.js +55 -0
  66. package/commitlint/config/factory.js +69 -0
  67. package/commitlint/config/plugins.js +227 -0
  68. package/commitlint/config/rules.js +155 -0
  69. package/commitlint/config/schema.js +46 -0
  70. package/commitlint/detection/dco.js +53 -0
  71. package/commitlint/detection/scopes.js +45 -0
  72. package/commitlint/formatter/format.js +85 -0
  73. package/commitlint/formatter/messages.js +79 -0
  74. package/commitlint/hook/diagnostics/branch.js +36 -0
  75. package/commitlint/hook/diagnostics/cache.js +37 -0
  76. package/commitlint/hook/diagnostics/commitlint-config.js +36 -0
  77. package/commitlint/hook/diagnostics/open-issues.js +56 -0
  78. package/commitlint/hook/diagnostics/package-manager.js +51 -0
  79. package/commitlint/hook/diagnostics/signing.js +107 -0
  80. package/commitlint/hook/envelope.js +46 -0
  81. package/commitlint/hook/output.js +45 -0
  82. package/commitlint/hook/parse-bash-command.js +105 -0
  83. package/commitlint/hook/rules/closes-trailer.js +31 -0
  84. package/commitlint/hook/rules/forbidden-content.js +32 -0
  85. package/commitlint/hook/rules/plan-leakage.js +36 -0
  86. package/commitlint/hook/rules/signing-flag-conflict.js +25 -0
  87. package/commitlint/hook/rules/soft-wrap.js +37 -0
  88. package/commitlint/hook/rules/types.js +14 -0
  89. package/commitlint/hook/rules/verbosity.js +31 -0
  90. package/commitlint/hook/silence-logger.js +39 -0
  91. package/commitlint/index.js +146 -0
  92. package/commitlint/prompt/config.js +91 -0
  93. package/commitlint/prompt/emojis.js +74 -0
  94. package/commitlint/prompt/prompter.js +135 -0
  95. package/commitlint/static.js +73 -0
  96. package/errors/BiomeSyncError.js +21 -0
  97. package/errors/ChangesetConfigError.js +20 -0
  98. package/errors/ConfigNotFoundError.js +21 -0
  99. package/errors/SectionParseError.js +16 -0
  100. package/errors/SectionValidationError.js +16 -0
  101. package/errors/SectionWriteError.js +16 -0
  102. package/errors/TagFormatError.js +20 -0
  103. package/errors/ToolNotFoundError.js +11 -0
  104. package/errors/ToolResolutionError.js +11 -0
  105. package/errors/ToolVersionMismatchError.js +11 -0
  106. package/errors/VersioningDetectionError.js +20 -0
  107. package/errors/WorkspaceAnalysisError.js +21 -0
  108. package/index.d.ts +9743 -8380
  109. package/index.js +36 -6657
  110. package/lint/Handler.js +39 -0
  111. package/lint/cli/sections.js +65 -0
  112. package/lint/cli/templates/markdownlint.gen.js +183 -0
  113. package/lint/config/Preset.js +152 -0
  114. package/lint/config/createConfig.js +89 -0
  115. package/lint/handlers/Biome.js +179 -0
  116. package/lint/handlers/Markdown.js +139 -0
  117. package/lint/handlers/PackageJson.js +130 -0
  118. package/lint/handlers/PnpmWorkspace.js +141 -0
  119. package/lint/handlers/ShellScripts.js +58 -0
  120. package/lint/handlers/TypeScript.js +134 -0
  121. package/lint/handlers/Yaml.js +167 -0
  122. package/lint/index.js +52 -0
  123. package/lint/utils/Command.js +285 -0
  124. package/lint/utils/Filter.js +100 -0
  125. package/lint/utils/Workspace.js +86 -0
  126. package/package.json +52 -63
  127. package/schemas/CommentStyle.js +16 -0
  128. package/schemas/ResolvedTool.js +63 -0
  129. package/schemas/SavvySections.js +113 -0
  130. package/schemas/SectionBlock.js +70 -0
  131. package/schemas/SectionDefinition.js +121 -0
  132. package/schemas/SectionResults.js +12 -0
  133. package/schemas/TagStrategySchemas.js +18 -0
  134. package/schemas/ToolDefinition.js +39 -0
  135. package/schemas/ToolResults.js +14 -0
  136. package/schemas/VersioningSchemas.js +95 -0
  137. package/schemas/WorkspaceAnalysisSchemas.js +190 -0
  138. package/services/BiomeSchemaSync.js +133 -0
  139. package/services/ChangesetConfig.js +78 -0
  140. package/services/ChangesetConfigReader.js +106 -0
  141. package/services/ConfigDiscovery.js +71 -0
  142. package/services/ManagedSection.js +288 -0
  143. package/services/SilkPublishability.js +193 -0
  144. package/services/SilkWorkspaceAnalyzer.js +213 -0
  145. package/services/TagStrategy.js +54 -0
  146. package/services/ToolDiscovery.js +229 -0
  147. package/services/VersioningStrategy.js +67 -0
  148. package/tsdoc-metadata.json +11 -11
  149. package/turbo/digest.js +127 -0
  150. package/turbo/errors.js +48 -0
  151. package/turbo/index.js +32 -0
  152. package/turbo/schemas/DryRun.js +57 -0
  153. package/turbo/schemas/results.js +61 -0
  154. package/turbo/services/TurboInspector.js +100 -0
  155. package/utils/ToolCommand.js +40 -0
@@ -0,0 +1,79 @@
1
+ //#region src/commitlint/formatter/messages.ts
2
+ /**
3
+ * Error message templates for the formatter.
4
+ *
5
+ * @internal
6
+ */
7
+ /**
8
+ * Detailed error explanations for common rule failures.
9
+ *
10
+ * @remarks
11
+ * Maps commitlint rule names to human-readable explanations of what
12
+ * the rule enforces and why the commit message failed.
13
+ *
14
+ * @public
15
+ */
16
+ const ERROR_EXPLANATIONS = {
17
+ "type-empty": "Commit messages must start with a type (e.g., feat, fix, docs).",
18
+ "type-enum": "The commit type must be one of: ai, build, chore, ci, docs, feat, fix, perf, refactor, release, revert, style, test.",
19
+ "subject-empty": "A subject describing the change is required after the type.",
20
+ "subject-case": "The subject should start with a lowercase letter.",
21
+ "subject-full-stop": "The subject should not end with a period.",
22
+ "header-max-length": "The first line of the commit message is too long. Keep it under 72 characters.",
23
+ "body-max-line-length": "Lines in the commit body should not exceed the configured maximum length.",
24
+ "body-leading-blank": "There should be a blank line between the subject and the body.",
25
+ "footer-leading-blank": "There should be a blank line between the body and the footer.",
26
+ "scope-enum": "The scope must be one of the allowed values for this project.",
27
+ "scope-empty": "A scope is required for this commit type.",
28
+ "signed-off-by": "This project requires DCO signoff. Add \"Signed-off-by: Your Name <email>\" to your commit, or use `git commit -s`.",
29
+ "silk/signed-off-by": "This project requires DCO signoff. Add \"Signed-off-by: Your Name <email>\" to your commit, or use `git commit -s`.",
30
+ "trailer-exists": "A required trailer is missing from the commit message.",
31
+ "references-empty": "This commit should reference an issue (e.g., #123 or PROJ-456)."
32
+ };
33
+ /**
34
+ * Suggestions for fixing common errors.
35
+ *
36
+ * @remarks
37
+ * Maps commitlint rule names to actionable suggestions for fixing
38
+ * the rule violation.
39
+ *
40
+ * @public
41
+ */
42
+ const ERROR_SUGGESTIONS = {
43
+ "type-empty": "Example: feat: add user authentication",
44
+ "type-enum": "Example: fix: resolve memory leak in cache",
45
+ "subject-empty": "Example: feat: add user authentication",
46
+ "subject-case": "Use lowercase: \"add feature\" instead of \"Add feature\"",
47
+ "subject-full-stop": "Remove the period: \"add feature\" instead of \"add feature.\"",
48
+ "header-max-length": "Move details to the commit body instead.",
49
+ "body-max-line-length": "Break long lines into multiple shorter lines.",
50
+ "body-leading-blank": "Add an empty line after the subject before the body.",
51
+ "footer-leading-blank": "Add an empty line before any footers like Signed-off-by.",
52
+ "signed-off-by": "Run: git commit --amend -s",
53
+ "silk/signed-off-by": "Run: git commit --amend -s"
54
+ };
55
+ /**
56
+ * Get explanation for a rule failure.
57
+ *
58
+ * @param ruleName - Name of the failed rule
59
+ * @returns Explanation string or undefined if no explanation exists
60
+ *
61
+ * @public
62
+ */
63
+ function getExplanation(ruleName) {
64
+ return ERROR_EXPLANATIONS[ruleName];
65
+ }
66
+ /**
67
+ * Get suggestion for fixing a rule failure.
68
+ *
69
+ * @param ruleName - Name of the failed rule
70
+ * @returns Suggestion string or undefined if no suggestion exists
71
+ *
72
+ * @public
73
+ */
74
+ function getSuggestion(ruleName) {
75
+ return ERROR_SUGGESTIONS[ruleName];
76
+ }
77
+
78
+ //#endregion
79
+ export { ERROR_EXPLANATIONS, ERROR_SUGGESTIONS, getExplanation, getSuggestion };
@@ -0,0 +1,36 @@
1
+ import { Effect } from "effect";
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+
5
+ //#region src/commitlint/hook/diagnostics/branch.ts
6
+ /**
7
+ * Branch and inferred-ticket detection.
8
+ *
9
+ * @internal
10
+ */
11
+ const execFileP = promisify(execFile);
12
+ const TICKET_RE = /^[a-z]+\/(\d+)[-/_]/;
13
+ function inferTicketId(branch) {
14
+ const m = branch.match(TICKET_RE);
15
+ return m ? Number(m[1]) : null;
16
+ }
17
+ 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();
25
+ return {
26
+ branch,
27
+ inferredTicketId: inferTicketId(branch)
28
+ };
29
+ }).pipe(Effect.orElseSucceed(() => ({
30
+ branch: null,
31
+ inferredTicketId: null
32
+ })));
33
+ }
34
+
35
+ //#endregion
36
+ export { inferTicketId, readBranchInfo };
@@ -0,0 +1,37 @@
1
+ import { Effect } from "effect";
2
+ import { dirname } from "node:path";
3
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
4
+
5
+ //#region src/commitlint/hook/diagnostics/cache.ts
6
+ /**
7
+ * JSON file cache with TTL. Writes are atomic-ish (mkdir -p, write,
8
+ * rename). Reads return null on any error or staleness — callers
9
+ * recompute silently.
10
+ *
11
+ * @internal
12
+ */
13
+ function readCache(path, ttlSeconds) {
14
+ return Effect.tryPromise(async () => {
15
+ const raw = await readFile(path, "utf8");
16
+ const env = JSON.parse(raw);
17
+ const cachedMs = Date.parse(env.cachedAt);
18
+ if (Number.isNaN(cachedMs)) return null;
19
+ if (Date.now() - cachedMs > ttlSeconds * 1e3) return null;
20
+ return env.data;
21
+ }).pipe(Effect.orElseSucceed(() => null));
22
+ }
23
+ function writeCache(path, data, when = /* @__PURE__ */ new Date()) {
24
+ return Effect.tryPromise(async () => {
25
+ await mkdir(dirname(path), { recursive: true });
26
+ const env = {
27
+ cachedAt: when.toISOString(),
28
+ data
29
+ };
30
+ const tmp = `${path}.tmp`;
31
+ await writeFile(tmp, JSON.stringify(env), "utf8");
32
+ await rename(tmp, path);
33
+ }).pipe(Effect.orElseSucceed(() => void 0));
34
+ }
35
+
36
+ //#endregion
37
+ export { readCache, writeCache };
@@ -0,0 +1,36 @@
1
+ import { isAbsolute, join, resolve } from "node:path";
2
+ import { readFile } from "node:fs/promises";
3
+
4
+ //#region src/commitlint/hook/diagnostics/commitlint-config.ts
5
+ /**
6
+ * Resolve the commitlint config path used by the husky commit-msg hook.
7
+ *
8
+ * The husky hook is the source of truth: `init` writes the chosen path into
9
+ * its managed section as `--config "$ROOT/<path>"`. The post-commit verifier
10
+ * extracts that same path so its replay matches the committed-time invocation.
11
+ *
12
+ * @internal
13
+ */
14
+ const CONFIG_FLAG_RE = /--config\s+(?:"([^"]+)"|'([^']+)'|(\S+))/;
15
+ const ROOT_PREFIXES = ["$ROOT/", "${ROOT}/"];
16
+ function parseHuskyConfigPath(huskyContent, root) {
17
+ const m = huskyContent.match(CONFIG_FLAG_RE);
18
+ if (!m) return null;
19
+ let raw = m[1] ?? m[2] ?? m[3] ?? "";
20
+ if (!raw) return null;
21
+ for (const prefix of ROOT_PREFIXES) if (raw.startsWith(prefix)) {
22
+ raw = raw.slice(prefix.length);
23
+ break;
24
+ }
25
+ return isAbsolute(raw) ? raw : resolve(root, raw);
26
+ }
27
+ async function readCommitlintConfigPath(root) {
28
+ try {
29
+ return parseHuskyConfigPath(await readFile(join(root, ".husky/commit-msg"), "utf8"), root);
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ //#endregion
36
+ export { parseHuskyConfigPath, readCommitlintConfigPath };
@@ -0,0 +1,56 @@
1
+ import { readCache, writeCache } from "./cache.js";
2
+ import { Effect } from "effect";
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+
6
+ //#region src/commitlint/hook/diagnostics/open-issues.ts
7
+ /**
8
+ * Open-issue lookup via gh CLI, cached on disk.
9
+ *
10
+ * @internal
11
+ */
12
+ const execFileP = promisify(execFile);
13
+ const ISSUES_CACHE_TTL_SECONDS = 600;
14
+ /** Relative path under CLAUDE_PROJECT_DIR where the open-issues cache lives. */
15
+ const ISSUES_CACHE_RELATIVE_PATH = ".claude/cache/issues.json";
16
+ function readOpenIssuesFromCache(cachePath, ttlSeconds = 600) {
17
+ return readCache(cachePath, ttlSeconds);
18
+ }
19
+ function fetchAndCacheOpenIssues(cachePath) {
20
+ return Effect.gen(function* () {
21
+ const repo = (yield* Effect.tryPromise(() => execFileP("gh", [
22
+ "repo",
23
+ "view",
24
+ "--json",
25
+ "nameWithOwner",
26
+ "-q",
27
+ ".nameWithOwner"
28
+ ]))).stdout.trim();
29
+ if (!repo) return null;
30
+ const listResult = yield* Effect.tryPromise(() => execFileP("gh", [
31
+ "issue",
32
+ "list",
33
+ "--repo",
34
+ repo,
35
+ "--state",
36
+ "open",
37
+ "--limit",
38
+ "20",
39
+ "--json",
40
+ "number,title"
41
+ ]));
42
+ const parsed = JSON.parse(listResult.stdout);
43
+ yield* writeCache(cachePath, parsed);
44
+ return parsed;
45
+ }).pipe(Effect.orElseSucceed(() => null));
46
+ }
47
+ function readOrFetchOpenIssues(cachePath) {
48
+ return Effect.gen(function* () {
49
+ const cached = yield* readOpenIssuesFromCache(cachePath);
50
+ if (cached !== null) return cached;
51
+ return yield* fetchAndCacheOpenIssues(cachePath);
52
+ });
53
+ }
54
+
55
+ //#endregion
56
+ export { ISSUES_CACHE_RELATIVE_PATH, fetchAndCacheOpenIssues, readOpenIssuesFromCache, readOrFetchOpenIssues };
@@ -0,0 +1,51 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { readFile } from "node:fs/promises";
4
+
5
+ //#region src/commitlint/hook/diagnostics/package-manager.ts
6
+ /**
7
+ * Package manager detection for the commit-time hooks.
8
+ *
9
+ * Mirrors the husky hook detection: prefer `package.json#packageManager`,
10
+ * fall back to lockfile presence in priority order pnpm \> yarn \> bun \> npm.
11
+ *
12
+ * @internal
13
+ */
14
+ const VALID_PMS = new Set([
15
+ "pnpm",
16
+ "yarn",
17
+ "bun",
18
+ "npm"
19
+ ]);
20
+ function parsePackageManagerField(packageJsonContent) {
21
+ let parsed;
22
+ try {
23
+ parsed = JSON.parse(packageJsonContent);
24
+ } catch {
25
+ return null;
26
+ }
27
+ const field = typeof parsed === "object" && parsed !== null && "packageManager" in parsed ? parsed.packageManager : void 0;
28
+ if (typeof field !== "string" || field.length === 0) return null;
29
+ const name = field.split("@")[0];
30
+ return VALID_PMS.has(name) ? name : null;
31
+ }
32
+ function detectFromLockfiles(presence) {
33
+ if (presence.pnpm) return "pnpm";
34
+ if (presence.yarn) return "yarn";
35
+ if (presence.bun) return "bun";
36
+ return "npm";
37
+ }
38
+ async function detectPackageManager(root) {
39
+ try {
40
+ const fromField = parsePackageManagerField(await readFile(join(root, "package.json"), "utf8"));
41
+ if (fromField !== null) return fromField;
42
+ } catch {}
43
+ return detectFromLockfiles({
44
+ pnpm: existsSync(join(root, "pnpm-lock.yaml")),
45
+ yarn: existsSync(join(root, "yarn.lock")),
46
+ bun: existsSync(join(root, "bun.lock"))
47
+ });
48
+ }
49
+
50
+ //#endregion
51
+ export { detectFromLockfiles, detectPackageManager, parsePackageManagerField };
@@ -0,0 +1,107 @@
1
+ import { Effect } from "effect";
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+ import { stat } from "node:fs/promises";
5
+
6
+ //#region src/commitlint/hook/diagnostics/signing.ts
7
+ /**
8
+ * GPG / SSH signing diagnostic.
9
+ *
10
+ * @internal
11
+ */
12
+ const execFileP = promisify(execFile);
13
+ function parseGpgKeyExpiry(colonsOutput) {
14
+ for (const line of colonsOutput.split("\n")) {
15
+ if (!line.startsWith("sec:")) continue;
16
+ const expires = line.split(":")[6];
17
+ if (expires && /^\d+$/.test(expires)) return (/* @__PURE__ */ new Date(Number(expires) * 1e3)).toISOString();
18
+ }
19
+ return null;
20
+ }
21
+ function buildSigningDiagnostic(raw) {
22
+ const signingKeyConfigured = !!raw.signingKey;
23
+ const format = !signingKeyConfigured ? "none" : raw.gpgFormat === "ssh" ? "ssh" : "gpg";
24
+ const autoSignEnabled = raw.commitGpgsign === "true";
25
+ const warnings = [];
26
+ if (!autoSignEnabled) warnings.push("commits will be unsigned (commit.gpgsign is not true)");
27
+ if (!signingKeyConfigured) warnings.push("user.signingkey is not configured");
28
+ if (signingKeyConfigured && !raw.keyResolves) warnings.push("user.signingkey does not resolve to an existing key/file");
29
+ if (raw.keyExpiry && Date.parse(raw.keyExpiry) < Date.now()) warnings.push(`signing key has expired (expired ${raw.keyExpiry})`);
30
+ if (format === "gpg" && !raw.agentResponsive) warnings.push("gpg-agent did not respond");
31
+ if (format === "ssh" && !raw.sshAllowedSignersFile) warnings.push("gpg.ssh.allowedSignersFile is unset; signature verification will fail");
32
+ return {
33
+ format,
34
+ autoSignEnabled,
35
+ signingKeyConfigured,
36
+ keyResolves: raw.keyResolves,
37
+ agentResponsive: raw.agentResponsive,
38
+ warnings
39
+ };
40
+ }
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
+ const FALLBACK_DIAGNOSTIC = {
55
+ format: "none",
56
+ autoSignEnabled: false,
57
+ signingKeyConfigured: false,
58
+ keyResolves: false,
59
+ agentResponsive: false,
60
+ warnings: ["signing diagnostic unavailable"]
61
+ };
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");
68
+ const isSsh = gpgFormat === "ssh";
69
+ let keyResolves = false;
70
+ let keyExpiry = null;
71
+ if (signingKey) if (isSsh) try {
72
+ await stat(signingKey);
73
+ keyResolves = true;
74
+ } catch {
75
+ keyResolves = false;
76
+ }
77
+ else try {
78
+ const { stdout } = await execFileP("gpg", [
79
+ "--list-secret-keys",
80
+ "--with-colons",
81
+ signingKey
82
+ ]);
83
+ keyResolves = stdout.trim().length > 0;
84
+ keyExpiry = parseGpgKeyExpiry(stdout);
85
+ } catch {
86
+ keyResolves = false;
87
+ }
88
+ let agentResponsive = true;
89
+ if (!isSsh) try {
90
+ await execFileP("gpg-connect-agent", ["/bye"], { timeout: 1e3 });
91
+ } catch {
92
+ agentResponsive = false;
93
+ }
94
+ return buildSigningDiagnostic({
95
+ gpgFormat,
96
+ commitGpgsign,
97
+ signingKey,
98
+ keyResolves,
99
+ agentResponsive,
100
+ keyExpiry,
101
+ sshAllowedSignersFile
102
+ });
103
+ }).pipe(Effect.orElseSucceed(() => FALLBACK_DIAGNOSTIC));
104
+ }
105
+
106
+ //#endregion
107
+ export { buildSigningDiagnostic, parseGpgKeyExpiry, readSigningDiagnostic };
@@ -0,0 +1,46 @@
1
+ import { Schema } from "effect";
2
+
3
+ //#region src/commitlint/hook/envelope.ts
4
+ /**
5
+ * Schemas for the JSON stdin envelopes Claude passes to plugin hooks.
6
+ *
7
+ * @remarks
8
+ * These are intentionally permissive — `Schema.Unknown` is used for fields
9
+ * we don't inspect (the agent may add unknown keys; we don't want a forward-
10
+ * compatibility break to make the hook fail).
11
+ *
12
+ * @internal
13
+ */
14
+ const PreToolUseEnvelope = Schema.Struct({
15
+ hook_event_name: Schema.Literal("PreToolUse"),
16
+ tool_name: Schema.String,
17
+ tool_input: Schema.Record({
18
+ key: Schema.String,
19
+ value: Schema.Unknown
20
+ })
21
+ });
22
+ const PostToolUseEnvelope = Schema.Struct({
23
+ hook_event_name: Schema.Literal("PostToolUse"),
24
+ tool_name: Schema.String,
25
+ tool_input: Schema.Record({
26
+ key: Schema.String,
27
+ value: Schema.Unknown
28
+ }),
29
+ tool_response: Schema.Struct({
30
+ interrupted: Schema.optional(Schema.Boolean),
31
+ exit_code: Schema.optional(Schema.Number),
32
+ stdout: Schema.optional(Schema.String),
33
+ stderr: Schema.optional(Schema.String)
34
+ })
35
+ });
36
+ const UserPromptSubmitEnvelope = Schema.Struct({
37
+ hook_event_name: Schema.Literal("UserPromptSubmit"),
38
+ prompt: Schema.String
39
+ });
40
+ const SessionStartEnvelope = Schema.Struct({
41
+ hook_event_name: Schema.Literal("SessionStart"),
42
+ source: Schema.optional(Schema.String)
43
+ });
44
+
45
+ //#endregion
46
+ export { PostToolUseEnvelope, PreToolUseEnvelope, SessionStartEnvelope, UserPromptSubmitEnvelope };
@@ -0,0 +1,45 @@
1
+ //#region src/commitlint/hook/output.ts
2
+ function preToolUseAllow(reason) {
3
+ return { hookSpecificOutput: {
4
+ hookEventName: "PreToolUse",
5
+ permissionDecision: "allow",
6
+ permissionDecisionReason: reason
7
+ } };
8
+ }
9
+ function preToolUseDeny(reason) {
10
+ return { hookSpecificOutput: {
11
+ hookEventName: "PreToolUse",
12
+ permissionDecision: "deny",
13
+ permissionDecisionReason: reason
14
+ } };
15
+ }
16
+ function preToolUseAdvise(message) {
17
+ return { hookSpecificOutput: {
18
+ hookEventName: "PreToolUse",
19
+ additionalContext: message
20
+ } };
21
+ }
22
+ function preToolUseSilent() {
23
+ return null;
24
+ }
25
+ function sessionStartContext(message) {
26
+ return { hookSpecificOutput: {
27
+ hookEventName: "SessionStart",
28
+ additionalContext: message
29
+ } };
30
+ }
31
+ function postToolUseAdvise(message) {
32
+ return { hookSpecificOutput: {
33
+ hookEventName: "PostToolUse",
34
+ additionalContext: message
35
+ } };
36
+ }
37
+ function userPromptSubmitContext(message) {
38
+ return { hookSpecificOutput: {
39
+ hookEventName: "UserPromptSubmit",
40
+ additionalContext: message
41
+ } };
42
+ }
43
+
44
+ //#endregion
45
+ export { postToolUseAdvise, preToolUseAdvise, preToolUseAllow, preToolUseDeny, preToolUseSilent, sessionStartContext, userPromptSubmitContext };
@@ -0,0 +1,105 @@
1
+ import { parse } from "shell-quote";
2
+
3
+ //#region src/commitlint/hook/parse-bash-command.ts
4
+ /**
5
+ * Parse a Bash command string to extract the commit message and flags
6
+ * for `git commit` / `gh pr create` / `gh pr edit` invocations.
7
+ *
8
+ * @remarks
9
+ * Only enough Bash is parsed to handle the shapes Claude actually emits.
10
+ * Heredocs surface as already-substituted strings inside Claude's tool
11
+ * envelope, so we don't need full shell semantics — just argv tokenization.
12
+ *
13
+ * Operators (`&&`, `||`, `|`, `;`) are filtered out by `shell-quote`. The
14
+ * parser inspects only `tokens[0]` and `tokens[1]`, so a compound command
15
+ * like `true && git commit -m "x"` will be classified as `unknown` rather
16
+ * than recursed into. That is the intended behaviour for the scaffold:
17
+ * compound shapes are uncommon in agent-emitted commands, and silently
18
+ * dropping them is safer than misattributing extracted state.
19
+ *
20
+ * @internal
21
+ */
22
+ function emptyFlags() {
23
+ return {
24
+ sign: "default",
25
+ noVerify: false,
26
+ amend: false
27
+ };
28
+ }
29
+ function parseBashCommand(command) {
30
+ const tokens = parse(command).filter((t) => typeof t === "string");
31
+ if (tokens.length === 0) return {
32
+ kind: "unknown",
33
+ message: null,
34
+ flags: emptyFlags(),
35
+ source: "none"
36
+ };
37
+ if (tokens[0] === "git" && tokens[1] === "commit") {
38
+ const flags = extractGitCommitFlags(tokens);
39
+ const kind = flags.amend ? "git-commit-amend" : "git-commit";
40
+ const message = extractGitCommitMessage(tokens);
41
+ return {
42
+ kind,
43
+ message,
44
+ flags,
45
+ source: message === null ? "none" : "inline"
46
+ };
47
+ }
48
+ if (tokens[0] === "gh" && tokens[1] === "pr" && (tokens[2] === "create" || tokens[2] === "edit")) {
49
+ const kind = tokens[2] === "create" ? "gh-pr-create" : "gh-pr-edit";
50
+ const message = extractFlagValue(tokens, "--body", "-b");
51
+ return {
52
+ kind,
53
+ message,
54
+ flags: emptyFlags(),
55
+ source: message === null ? "none" : "inline"
56
+ };
57
+ }
58
+ return {
59
+ kind: "unknown",
60
+ message: null,
61
+ flags: emptyFlags(),
62
+ source: "none"
63
+ };
64
+ }
65
+ function extractGitCommitFlags(tokens) {
66
+ let sign = "default";
67
+ let noVerify = false;
68
+ let amend = false;
69
+ for (const tok of tokens.slice(2)) if (tok === "-S" || tok === "--gpg-sign" || tok.startsWith("--gpg-sign=")) sign = "force-on";
70
+ else if (tok === "--no-gpg-sign") sign = "force-off";
71
+ else if (tok === "--no-verify" || tok === "-n") noVerify = true;
72
+ else if (tok === "--amend") amend = true;
73
+ return {
74
+ sign,
75
+ noVerify,
76
+ amend
77
+ };
78
+ }
79
+ function extractGitCommitMessage(tokens) {
80
+ const parts = [];
81
+ for (let i = 2; i < tokens.length; i++) {
82
+ const tok = tokens[i];
83
+ if (tok === void 0) continue;
84
+ if (tok === "-m" || tok === "--message") {
85
+ const next = tokens[i + 1];
86
+ if (next !== void 0) {
87
+ parts.push(next);
88
+ i += 1;
89
+ }
90
+ } else if (tok.startsWith("--message=")) parts.push(tok.slice(10));
91
+ }
92
+ return parts.length > 0 ? parts.join("\n\n") : null;
93
+ }
94
+ function extractFlagValue(tokens, ...names) {
95
+ for (let i = 0; i < tokens.length; i++) {
96
+ const tok = tokens[i];
97
+ if (tok === void 0) continue;
98
+ if (names.includes(tok)) return tokens[i + 1] ?? null;
99
+ for (const name of names) if (tok.startsWith(`${name}=`)) return tok.slice(name.length + 1);
100
+ }
101
+ return null;
102
+ }
103
+
104
+ //#endregion
105
+ export { parseBashCommand };
@@ -0,0 +1,31 @@
1
+ import { Effect } from "effect";
2
+
3
+ //#region src/commitlint/hook/rules/closes-trailer.ts
4
+ /**
5
+ * closes-trailer rule — advises when the branch encodes a ticket id but
6
+ * the message body has no Closes/Fixes/Resolves trailer for it.
7
+ *
8
+ * @internal
9
+ */
10
+ function hasClosingTrailer(message, ticketId) {
11
+ return new RegExp(`\\b(closes|fixes|resolves)\\s+#${ticketId}\\b`, "i").test(message);
12
+ }
13
+ const closesTrailerRule = {
14
+ id: "closes-trailer",
15
+ severity: "advise",
16
+ check: (input, ctx) => Effect.sync(() => {
17
+ const tid = ctx.branchInfo.inferredTicketId;
18
+ if (tid === null) return null;
19
+ if (hasClosingTrailer(input.message, tid)) return null;
20
+ const openList = ctx.openIssues.map((i) => ` #${i.number} ${i.title}`).join("\n");
21
+ const tail = openList.length > 0 ? `\n\nOpen issues in this repo:\n${openList}` : "";
22
+ return {
23
+ ruleId: "closes-trailer",
24
+ severity: "advise",
25
+ message: `Branch ${ctx.branchInfo.branch} looks like ticket #${tid} but the message has no Closes/Fixes/Resolves #${tid} trailer. If this commit closes #${tid}, add 'Closes #${tid}' above the Signed-off-by line.${tail}`
26
+ };
27
+ })
28
+ };
29
+
30
+ //#endregion
31
+ export { closesTrailerRule, hasClosingTrailer };
@@ -0,0 +1,32 @@
1
+ import { Effect } from "effect";
2
+
3
+ //#region src/commitlint/hook/rules/forbidden-content.ts
4
+ /**
5
+ * forbidden-content rule — denies markdown headers and code fences in
6
+ * commit message bodies.
7
+ *
8
+ * @internal
9
+ */
10
+ const forbiddenContentRule = {
11
+ id: "forbidden-content",
12
+ severity: "deny",
13
+ check: (input) => Effect.sync(() => {
14
+ const lines = input.message.split("\n");
15
+ for (const line of lines) {
16
+ if (/^#{1,6}\s/.test(line)) return {
17
+ ruleId: "forbidden-content",
18
+ severity: "deny",
19
+ message: "Body contains a markdown header (#, ##, ...). Headers are forbidden by the @savvy-web/commitlint Silk preset. Remove the header line and rewrite as plain prose or a bullet."
20
+ };
21
+ if (/^```/.test(line)) return {
22
+ ruleId: "forbidden-content",
23
+ severity: "deny",
24
+ message: "Body contains a code fence (```). Code fences are forbidden in commit bodies. Move code samples to the PR description."
25
+ };
26
+ }
27
+ return null;
28
+ })
29
+ };
30
+
31
+ //#endregion
32
+ export { forbiddenContentRule };