@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,167 @@
1
+ import { Command } from "../utils/Command.js";
2
+ import { Filter } from "../utils/Filter.js";
3
+ import { getWorkspaceRoot } from "../utils/Workspace.js";
4
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { format, resolveConfig } from "prettier";
7
+ import { lint } from "yaml-lint";
8
+
9
+ //#region src/lint/handlers/Yaml.ts
10
+ /**
11
+ * Handler for YAML files.
12
+ *
13
+ * Formats with Prettier and validates with yaml-lint, both as bundled dependencies.
14
+ */
15
+ /**
16
+ * Handler for YAML files.
17
+ *
18
+ * Formats with Prettier and validates with yaml-lint, both as bundled dependencies.
19
+ *
20
+ * @remarks
21
+ * Excludes pnpm-lock.yaml and pnpm-workspace.yaml by default.
22
+ * pnpm-workspace.yaml has its own dedicated handler.
23
+ *
24
+ * Uses Prettier for formatting and yaml-lint for validation.
25
+ * Both are bundled dependencies (no CLI spawning required).
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * import { Yaml } from '\@savvy-web/lint-staged';
30
+ *
31
+ * export default {
32
+ * [Yaml.glob]: Yaml.create({
33
+ * exclude: ['pnpm-lock.yaml', 'pnpm-workspace.yaml', 'generated/'],
34
+ * }),
35
+ * };
36
+ * ```
37
+ */
38
+ var Yaml = class Yaml {
39
+ /**
40
+ * Glob pattern for matching YAML files.
41
+ * @defaultValue `'**\/*.{yml,yaml}'`
42
+ */
43
+ static glob = "**/*.{yml,yaml}";
44
+ /**
45
+ * Default patterns to exclude from processing.
46
+ * @defaultValue `['pnpm-lock.yaml', 'pnpm-workspace.yaml', '__test__/fixtures']`
47
+ */
48
+ static defaultExcludes = [
49
+ "pnpm-lock.yaml",
50
+ "pnpm-workspace.yaml",
51
+ "__test__/fixtures"
52
+ ];
53
+ /**
54
+ * Pre-configured handler with default options.
55
+ */
56
+ static handler = Yaml.create();
57
+ /**
58
+ * Find the yaml-lint config file.
59
+ *
60
+ * Paths are anchored to the workspace root (via {@link getWorkspaceRoot}),
61
+ * falling back to `process.cwd()` when not inside a workspace.
62
+ *
63
+ * Searches in order:
64
+ * 1. `{workspaceRoot}/lib/configs/.yaml-lint.json`
65
+ * 2. `{workspaceRoot}/.yaml-lint.json`
66
+ *
67
+ * @returns The config file path, or undefined if not found
68
+ */
69
+ static findConfig() {
70
+ const root = getWorkspaceRoot() ?? process.cwd();
71
+ const libPath = join(root, "lib/configs/.yaml-lint.json");
72
+ if (existsSync(libPath)) return libPath;
73
+ const rootPath = join(root, ".yaml-lint.json");
74
+ if (existsSync(rootPath)) return rootPath;
75
+ }
76
+ /**
77
+ * Load the yaml-lint schema from a config file.
78
+ *
79
+ * @param filepath - Path to the yaml-lint config file
80
+ * @returns The schema string, or undefined if not found
81
+ */
82
+ static loadConfig(filepath) {
83
+ try {
84
+ const content = readFileSync(filepath, "utf-8");
85
+ return JSON.parse(content).schema;
86
+ } catch {
87
+ return;
88
+ }
89
+ }
90
+ /**
91
+ * Check if yaml-lint is available.
92
+ *
93
+ * @returns Always `true` since yaml-lint is a bundled dependency
94
+ */
95
+ static isAvailable() {
96
+ return true;
97
+ }
98
+ /**
99
+ * Format a YAML file in-place using Prettier.
100
+ *
101
+ * @param filepath - Path to the YAML file
102
+ */
103
+ static async formatFile(filepath) {
104
+ writeFileSync(filepath, await format(readFileSync(filepath, "utf-8"), {
105
+ ...await resolveConfig(filepath),
106
+ filepath,
107
+ parser: "yaml"
108
+ }), "utf-8");
109
+ }
110
+ /**
111
+ * Validate a YAML file using yaml-lint.
112
+ *
113
+ * @param filepath - Path to the YAML file
114
+ * @param schema - The YAML schema to validate against
115
+ * @throws Error if the YAML is invalid
116
+ */
117
+ static async validateFile(filepath, schema) {
118
+ await lint(readFileSync(filepath, "utf-8"), schema ? { schema } : void 0);
119
+ }
120
+ /**
121
+ * Create a handler that returns a CLI command to format YAML files.
122
+ *
123
+ * @remarks
124
+ * Unlike {@link create}, this does not modify files in the handler function
125
+ * body. Instead it returns a `savvy-lint fmt yaml` command so lint-staged
126
+ * can detect the modification and auto-stage it.
127
+ * Use this in lint-staged array syntax for sequential execution.
128
+ *
129
+ * @param options - Configuration options
130
+ * @returns A lint-staged compatible handler function
131
+ */
132
+ static fmtCommand(options = {}) {
133
+ const excludes = options.exclude ?? [...Yaml.defaultExcludes];
134
+ return (filenames) => {
135
+ const filtered = Filter.exclude(filenames, excludes);
136
+ if (filtered.length === 0) return [];
137
+ return `${Command.findSavvyLint()} fmt yaml ${Filter.shellEscape(filtered)}`;
138
+ };
139
+ }
140
+ /**
141
+ * Create a handler with custom options.
142
+ *
143
+ * @param options - Configuration options
144
+ * @returns A lint-staged compatible handler function
145
+ */
146
+ static create(options = {}) {
147
+ const excludes = options.exclude ?? [...Yaml.defaultExcludes];
148
+ const skipFormat = options.skipFormat ?? false;
149
+ const skipValidate = options.skipValidate ?? false;
150
+ const configPath = options.config ?? Yaml.findConfig();
151
+ const schema = configPath ? Yaml.loadConfig(configPath) : void 0;
152
+ return async (filenames) => {
153
+ const filtered = Filter.exclude(filenames, excludes);
154
+ if (filtered.length === 0) return [];
155
+ if (!skipFormat) for (const filepath of filtered) await Yaml.formatFile(filepath);
156
+ if (!skipValidate) for (const filepath of filtered) try {
157
+ await Yaml.validateFile(filepath, schema);
158
+ } catch (error) {
159
+ throw new Error(`Invalid YAML in ${filepath}: ${error instanceof Error ? error.message : String(error)}`);
160
+ }
161
+ return [];
162
+ };
163
+ }
164
+ };
165
+
166
+ //#endregion
167
+ export { Yaml };
package/lint/index.js ADDED
@@ -0,0 +1,52 @@
1
+ import { __exportAll } from "../_virtual/_rolldown/runtime.js";
2
+ import { Handler } from "./Handler.js";
3
+ import { Command } from "./utils/Command.js";
4
+ import { Filter } from "./utils/Filter.js";
5
+ import { getWorkspacePackagePaths, getWorkspacePackages, getWorkspaceRoot, isWorkspacePackagePath, resetWorkspaceCache } from "./utils/Workspace.js";
6
+ import { Biome } from "./handlers/Biome.js";
7
+ import { Markdown } from "./handlers/Markdown.js";
8
+ import { PackageJson } from "./handlers/PackageJson.js";
9
+ import { PnpmWorkspace } from "./handlers/PnpmWorkspace.js";
10
+ import { ShellScripts } from "./handlers/ShellScripts.js";
11
+ import { TypeScript } from "./handlers/TypeScript.js";
12
+ import { Yaml } from "./handlers/Yaml.js";
13
+ import { createConfig } from "./config/createConfig.js";
14
+ import { Preset } from "./config/Preset.js";
15
+ import { DEFAULT_CONFIG_PATH, HUSKY_HOOK_PATH, LegacySavvyLintHygieneDef, MARKDOWNLINT_CONFIG_PATH, POST_CHECKOUT_HOOK_PATH, POST_MERGE_HOOK_PATH, SavvyLintSectionDef, generateManagedContent, savvyLintBlock } from "./cli/sections.js";
16
+ import { MARKDOWNLINT_CONFIG, MARKDOWNLINT_SCHEMA, MARKDOWNLINT_TEMPLATE } from "./cli/templates/markdownlint.gen.js";
17
+
18
+ //#region src/lint/index.ts
19
+ var lint_exports = /* @__PURE__ */ __exportAll({
20
+ Biome: () => Biome,
21
+ Command: () => Command,
22
+ DEFAULT_CONFIG_PATH: () => DEFAULT_CONFIG_PATH,
23
+ Filter: () => Filter,
24
+ HUSKY_HOOK_PATH: () => HUSKY_HOOK_PATH,
25
+ Handler: () => Handler,
26
+ LegacySavvyLintHygieneDef: () => LegacySavvyLintHygieneDef,
27
+ MARKDOWNLINT_CONFIG: () => MARKDOWNLINT_CONFIG,
28
+ MARKDOWNLINT_CONFIG_PATH: () => MARKDOWNLINT_CONFIG_PATH,
29
+ MARKDOWNLINT_SCHEMA: () => MARKDOWNLINT_SCHEMA,
30
+ MARKDOWNLINT_TEMPLATE: () => MARKDOWNLINT_TEMPLATE,
31
+ Markdown: () => Markdown,
32
+ POST_CHECKOUT_HOOK_PATH: () => POST_CHECKOUT_HOOK_PATH,
33
+ POST_MERGE_HOOK_PATH: () => POST_MERGE_HOOK_PATH,
34
+ PackageJson: () => PackageJson,
35
+ PnpmWorkspace: () => PnpmWorkspace,
36
+ Preset: () => Preset,
37
+ SavvyLintSectionDef: () => SavvyLintSectionDef,
38
+ ShellScripts: () => ShellScripts,
39
+ TypeScript: () => TypeScript,
40
+ Yaml: () => Yaml,
41
+ createConfig: () => createConfig,
42
+ generateManagedContent: () => generateManagedContent,
43
+ getWorkspacePackagePaths: () => getWorkspacePackagePaths,
44
+ getWorkspacePackages: () => getWorkspacePackages,
45
+ getWorkspaceRoot: () => getWorkspaceRoot,
46
+ isWorkspacePackagePath: () => isWorkspacePackagePath,
47
+ resetWorkspaceCache: () => resetWorkspaceCache,
48
+ savvyLintBlock: () => savvyLintBlock
49
+ });
50
+
51
+ //#endregion
52
+ export { lint_exports };
@@ -0,0 +1,285 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { execSync } from "node:child_process";
4
+
5
+ //#region src/lint/utils/Command.ts
6
+ /**
7
+ * Utilities for shell command operations.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { Command } from '@savvy-web/lint-staged';
12
+ *
13
+ * if (Command.isAvailable('yq')) {
14
+ * // yq is installed globally
15
+ * }
16
+ *
17
+ * // Check for tool available globally OR via package manager
18
+ * const biome = Command.findTool('biome');
19
+ * if (biome.available) {
20
+ * console.log(`Using: ${biome.command}`); // 'biome' or 'pnpm exec biome'
21
+ * }
22
+ *
23
+ * // Detect package manager from package.json
24
+ * const pm = Command.detectPackageManager();
25
+ * console.log(pm); // 'pnpm', 'npm', 'yarn', or 'bun'
26
+ * ```
27
+ */
28
+ /** Pattern for valid command/tool names (alphanumeric, hyphens, underscores, \@, /) */
29
+ const VALID_COMMAND_PATTERN = /^[\w@/-]+$/;
30
+ /**
31
+ * Validate that a command name is safe for shell execution.
32
+ * Prevents command injection by ensuring only valid characters are used.
33
+ *
34
+ * @param name - The command or tool name to validate
35
+ * @throws Error if the name contains invalid characters
36
+ */
37
+ function validateCommandName(name) {
38
+ if (!VALID_COMMAND_PATTERN.test(name)) throw new Error(`Invalid command name: "${name}". Only alphanumeric characters, hyphens, underscores, @ and / are allowed.`);
39
+ }
40
+ /**
41
+ * Static utility class for shell command operations.
42
+ */
43
+ var Command = class Command {
44
+ /** Cached package manager detection result */
45
+ static cachedPackageManager = null;
46
+ /** Cached project root path */
47
+ static cachedRoot = null;
48
+ /**
49
+ * Find the project root directory by walking up from `cwd`.
50
+ *
51
+ * Searches upward for the nearest directory containing a `package.json`.
52
+ * Falls back to the provided `cwd` (or `process.cwd()`) when none is found.
53
+ *
54
+ * @remarks
55
+ * This is more reliable than `process.cwd()` in environments like Husky
56
+ * hooks where the working directory may point to `.husky/` or another
57
+ * subdirectory.
58
+ *
59
+ * @param cwd - Starting directory for the search (defaults to `process.cwd()`)
60
+ * @returns The resolved project root path
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * const root = Command.findRoot();
65
+ * console.log(root); // '/Users/me/my-project'
66
+ * ```
67
+ */
68
+ static findRoot(cwd = process.cwd()) {
69
+ if (Command.cachedRoot !== null) return Command.cachedRoot;
70
+ let dir = resolve(cwd);
71
+ while (true) {
72
+ if (existsSync(join(dir, "package.json"))) {
73
+ Command.cachedRoot = dir;
74
+ return dir;
75
+ }
76
+ const parent = dirname(dir);
77
+ if (parent === dir) break;
78
+ dir = parent;
79
+ }
80
+ Command.cachedRoot = cwd;
81
+ return cwd;
82
+ }
83
+ /**
84
+ * Detect the package manager from the root package.json's `packageManager` field.
85
+ *
86
+ * Parses the `packageManager` field (e.g., `pnpm\@9.0.0`) and extracts the manager name.
87
+ * Falls back to "npm" if no packageManager field is found.
88
+ *
89
+ * @param cwd - Directory to search for package.json (defaults to `Command.findRoot()`)
90
+ * @returns The detected package manager
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * const pm = Command.detectPackageManager();
95
+ * console.log(pm); // 'pnpm', 'npm', 'yarn', or 'bun'
96
+ * ```
97
+ */
98
+ static detectPackageManager(cwd = Command.findRoot()) {
99
+ if (Command.cachedPackageManager !== null) return Command.cachedPackageManager;
100
+ const packageJsonPath = join(cwd, "package.json");
101
+ if (!existsSync(packageJsonPath)) {
102
+ Command.cachedPackageManager = "npm";
103
+ return "npm";
104
+ }
105
+ try {
106
+ const content = readFileSync(packageJsonPath, "utf-8");
107
+ const pkg = JSON.parse(content);
108
+ if (pkg.packageManager) {
109
+ const match = pkg.packageManager.match(/^(npm|pnpm|yarn|bun)@/);
110
+ if (match) {
111
+ Command.cachedPackageManager = match[1];
112
+ return Command.cachedPackageManager;
113
+ }
114
+ }
115
+ } catch {}
116
+ Command.cachedPackageManager = "npm";
117
+ return "npm";
118
+ }
119
+ /**
120
+ * Get the exec command prefix for a package manager.
121
+ *
122
+ * @param packageManager - The package manager name
123
+ * @returns Array of command parts to prefix tool execution
124
+ *
125
+ * @example
126
+ * ```typescript
127
+ * Command.getExecPrefix('pnpm'); // ['pnpm', 'exec']
128
+ * Command.getExecPrefix('npm'); // ['npx', '--no']
129
+ * Command.getExecPrefix('yarn'); // ['yarn', 'exec']
130
+ * Command.getExecPrefix('bun'); // ['bun', 'x', "--no-install"]
131
+ * ```
132
+ */
133
+ static getExecPrefix(packageManager) {
134
+ switch (packageManager) {
135
+ case "pnpm": return ["pnpm", "exec"];
136
+ case "yarn": return ["yarn", "exec"];
137
+ case "bun": return [
138
+ "bun",
139
+ "x",
140
+ "--no-install"
141
+ ];
142
+ default: return ["npx", "--no"];
143
+ }
144
+ }
145
+ /**
146
+ * Clear the cached package manager and project root detection.
147
+ * Useful for testing or when package.json changes.
148
+ */
149
+ static clearCache() {
150
+ Command.cachedPackageManager = null;
151
+ Command.cachedRoot = null;
152
+ }
153
+ /**
154
+ * Check if a command is available in the system PATH.
155
+ *
156
+ * @param command - The command name to check
157
+ * @returns `true` if the command exists, `false` otherwise
158
+ *
159
+ * @example
160
+ * ```typescript
161
+ * if (Command.isAvailable('yq')) {
162
+ * console.log('yq is installed');
163
+ * }
164
+ * ```
165
+ */
166
+ static isAvailable(command) {
167
+ validateCommandName(command);
168
+ try {
169
+ execSync(`command -v ${command}`, { stdio: "ignore" });
170
+ return true;
171
+ } catch {
172
+ return false;
173
+ }
174
+ }
175
+ /**
176
+ * Find a tool, checking global installation first, then the project's package manager.
177
+ *
178
+ * Search order:
179
+ * 1. Global command (in PATH)
180
+ * 2. Project's package manager (detected from package.json `packageManager` field)
181
+ *
182
+ * @param tool - The tool name to find
183
+ * @returns Search result with command string if found
184
+ *
185
+ * @example
186
+ * ```typescript
187
+ * const biome = Command.findTool('biome');
188
+ * if (biome.available) {
189
+ * // biome.command is 'biome', 'pnpm exec biome', 'npx --no biome', etc.
190
+ * console.log(`Running: ${biome.command} check`);
191
+ * }
192
+ * ```
193
+ */
194
+ static findTool(tool) {
195
+ validateCommandName(tool);
196
+ if (Command.isAvailable(tool)) return {
197
+ available: true,
198
+ command: tool,
199
+ source: "global"
200
+ };
201
+ const pm = Command.detectPackageManager();
202
+ const execCmd = [...Command.getExecPrefix(pm), tool].join(" ");
203
+ try {
204
+ execSync(`${execCmd} --version`, { stdio: "ignore" });
205
+ return {
206
+ available: true,
207
+ command: execCmd,
208
+ source: pm
209
+ };
210
+ } catch {}
211
+ return {
212
+ available: false,
213
+ command: void 0,
214
+ source: void 0
215
+ };
216
+ }
217
+ /**
218
+ * Find a tool or throw an error if not available.
219
+ *
220
+ * @param tool - The tool name to find
221
+ * @param errorMessage - Custom error message (optional)
222
+ * @returns The command string to use
223
+ * @throws Error if the tool is not available
224
+ *
225
+ * @example
226
+ * ```typescript
227
+ * const biomeCmd = Command.requireTool('biome');
228
+ * // Throws if biome not found, otherwise returns command string
229
+ * ```
230
+ */
231
+ static requireTool(tool, errorMessage) {
232
+ const result = Command.findTool(tool);
233
+ if (!result.available || !result.command) throw new Error(errorMessage ?? `Required tool '${tool}' is not available. Install it globally or add it as a dev dependency.`);
234
+ return result.command;
235
+ }
236
+ /**
237
+ * Find the `savvy lint` CLI command.
238
+ *
239
+ * @remarks
240
+ * Resolves the unified `savvy` binary via the standard tool search and
241
+ * appends the `lint` subcommand, falling back to the cli dev build at
242
+ * `packages/cli/dist/dev/pkg/bin/savvy.js` for dogfooding scenarios where the
243
+ * bin isn't linked yet.
244
+ *
245
+ * @returns The command string to invoke `savvy lint`
246
+ */
247
+ static findSavvyLint() {
248
+ const result = Command.findTool("savvy");
249
+ if (result.available && result.command) return `${result.command} lint`;
250
+ return `node ${Command.findRoot()}/packages/cli/dist/dev/pkg/bin/savvy.js lint`;
251
+ }
252
+ /**
253
+ * Execute a command and return its output.
254
+ *
255
+ * @param command - The command to execute
256
+ * @returns The command output as a string (trimmed)
257
+ * @throws If the command fails
258
+ *
259
+ * @example
260
+ * ```typescript
261
+ * const version = Command.exec('node --version');
262
+ * console.log(version); // 'v20.10.0'
263
+ * ```
264
+ */
265
+ static exec(command) {
266
+ return execSync(command, { encoding: "utf-8" }).trim();
267
+ }
268
+ /**
269
+ * Execute a command silently (ignore output).
270
+ *
271
+ * @param command - The command to execute
272
+ * @returns `true` if successful, `false` if failed
273
+ */
274
+ static execSilent(command) {
275
+ try {
276
+ execSync(command, { stdio: "ignore" });
277
+ return true;
278
+ } catch {
279
+ return false;
280
+ }
281
+ }
282
+ };
283
+
284
+ //#endregion
285
+ export { Command };
@@ -0,0 +1,100 @@
1
+ //#region src/lint/utils/Filter.ts
2
+ /**
3
+ * Utilities for filtering staged file lists.
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * import { Filter } from '@savvy-web/lint-staged';
8
+ *
9
+ * const handler = (filenames: readonly string[]) => {
10
+ * const filtered = Filter.exclude(filenames, ['dist/', '__fixtures__']);
11
+ * return filtered.length > 0 ? `biome check ${Filter.shellEscape(filtered)}` : [];
12
+ * };
13
+ * ```
14
+ */
15
+ /**
16
+ * Static utility class for filtering file lists.
17
+ */
18
+ var Filter = class Filter {
19
+ /**
20
+ * Exclude files matching any of the given patterns.
21
+ *
22
+ * @param filenames - Array of file paths
23
+ * @param patterns - Patterns to exclude (uses `string.includes()`)
24
+ * @returns Filtered array of file paths
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * const files = ['src/index.ts', 'dist/index.js', '__fixtures__/test.ts'];
29
+ * const filtered = Filter.exclude(files, ['dist/', '__fixtures__']);
30
+ * // Result: ['src/index.ts']
31
+ * ```
32
+ */
33
+ static exclude(filenames, patterns) {
34
+ if (patterns.length === 0) return [...filenames];
35
+ return filenames.filter((file) => !patterns.some((pattern) => file.includes(pattern)));
36
+ }
37
+ /**
38
+ * Include only files matching any of the given patterns.
39
+ *
40
+ * @param filenames - Array of file paths
41
+ * @param patterns - Patterns to include (uses `string.includes()`)
42
+ * @returns Filtered array of file paths
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * const files = ['src/index.ts', 'lib/utils.ts', 'test/foo.test.ts'];
47
+ * const filtered = Filter.include(files, ['src/', 'lib/']);
48
+ * // Result: ['src/index.ts', 'lib/utils.ts']
49
+ * ```
50
+ */
51
+ static include(filenames, patterns) {
52
+ if (patterns.length === 0) return [];
53
+ return filenames.filter((file) => patterns.some((pattern) => file.includes(pattern)));
54
+ }
55
+ /**
56
+ * Combine exclude and include filters.
57
+ *
58
+ * @param filenames - Array of file paths
59
+ * @param options - Filter options
60
+ * @returns Filtered array of file paths
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * const files = ['src/index.ts', 'src/index.test.ts', 'dist/index.js'];
65
+ * const filtered = Filter.apply(files, {
66
+ * include: ['src/'],
67
+ * exclude: ['.test.'],
68
+ * });
69
+ * // Result: ['src/index.ts']
70
+ * ```
71
+ */
72
+ static apply(filenames, options) {
73
+ let result = [...filenames];
74
+ if (options.include && options.include.length > 0) result = Filter.include(result, options.include);
75
+ if (options.exclude && options.exclude.length > 0) result = Filter.exclude(result, options.exclude);
76
+ return result;
77
+ }
78
+ /**
79
+ * Escape file paths for safe shell command construction.
80
+ *
81
+ * Wraps each path in single quotes and escapes any embedded single quotes.
82
+ * This prevents issues with paths containing spaces or special characters.
83
+ *
84
+ * @param filenames - Array of file paths
85
+ * @returns Space-separated string of shell-escaped paths
86
+ *
87
+ * @example
88
+ * ```typescript
89
+ * const files = ['/path/to/file.ts', '/path/with spaces/file.ts'];
90
+ * const escaped = Filter.shellEscape(files);
91
+ * // Result: "'/path/to/file.ts' '/path/with spaces/file.ts'"
92
+ * ```
93
+ */
94
+ static shellEscape(filenames) {
95
+ return filenames.map((f) => `'${f.replace(/'/g, "'\\''")}'`).join(" ");
96
+ }
97
+ };
98
+
99
+ //#endregion
100
+ export { Filter };
@@ -0,0 +1,86 @@
1
+ import { dirname } from "node:path";
2
+ import { findWorkspaceRootSync, getWorkspacePackagesSync } from "workspaces-effect";
3
+
4
+ //#region src/lint/utils/Workspace.ts
5
+ /**
6
+ * Workspace-aware discovery utilities.
7
+ *
8
+ * @remarks
9
+ * Wraps the synchronous APIs from `workspaces-effect` with caching.
10
+ * Workspace layout does not change during a lint-staged run, so
11
+ * results are cached on first access. Use `resetWorkspaceCache()`
12
+ * in tests to clear state between runs.
13
+ */
14
+ /** Sentinel indicating "not yet resolved". */
15
+ const UNRESOLVED = Symbol("unresolved");
16
+ let cachedRoot = UNRESOLVED;
17
+ let cachedPackages = UNRESOLVED;
18
+ let cachedPaths = UNRESOLVED;
19
+ /**
20
+ * Get the workspace root directory.
21
+ *
22
+ * @returns Absolute path to workspace root, or null if not in a workspace
23
+ */
24
+ function getWorkspaceRoot() {
25
+ if (cachedRoot !== UNRESOLVED) return cachedRoot;
26
+ cachedRoot = findWorkspaceRootSync() ?? null;
27
+ return cachedRoot;
28
+ }
29
+ /**
30
+ * Get all leaf workspace packages (excludes root).
31
+ *
32
+ * @returns Array of workspace packages, or null if not in a workspace
33
+ */
34
+ function getWorkspacePackages() {
35
+ if (cachedPackages !== UNRESOLVED) return cachedPackages;
36
+ const root = getWorkspaceRoot();
37
+ if (root === null) {
38
+ cachedPackages = null;
39
+ return null;
40
+ }
41
+ cachedPackages = (getWorkspacePackagesSync(root) ?? []).filter((pkg) => pkg.path !== root);
42
+ return cachedPackages;
43
+ }
44
+ /**
45
+ * Get absolute paths of all leaf workspace package directories.
46
+ *
47
+ * @returns Array of absolute paths, empty if not in a workspace
48
+ */
49
+ function getWorkspacePackagePaths() {
50
+ if (cachedPaths !== UNRESOLVED) return cachedPaths;
51
+ cachedPaths = getWorkspacePackages()?.map((pkg) => pkg.path) ?? [];
52
+ return cachedPaths;
53
+ }
54
+ /**
55
+ * Check if a file path is at a workspace root or leaf workspace root.
56
+ *
57
+ * @remarks
58
+ * Compares the file's parent directory against the workspace root
59
+ * and all leaf workspace roots. Returns true as a permissive
60
+ * fallback when not in a workspace, so single-package repos
61
+ * continue to work.
62
+ *
63
+ * @param filePath - Absolute path to the file
64
+ * @returns true if the file is at a workspace or leaf root
65
+ */
66
+ function isWorkspacePackagePath(filePath) {
67
+ const root = getWorkspaceRoot();
68
+ if (root === null) return true;
69
+ const dir = dirname(filePath);
70
+ if (dir === root) return true;
71
+ return getWorkspacePackagePaths().includes(dir);
72
+ }
73
+ /**
74
+ * Clear all cached workspace data.
75
+ *
76
+ * @remarks
77
+ * Call this in test teardown to ensure clean state between tests.
78
+ */
79
+ function resetWorkspaceCache() {
80
+ cachedRoot = UNRESOLVED;
81
+ cachedPackages = UNRESOLVED;
82
+ cachedPaths = UNRESOLVED;
83
+ }
84
+
85
+ //#endregion
86
+ export { getWorkspacePackagePaths, getWorkspacePackages, getWorkspaceRoot, isWorkspacePackagePath, resetWorkspaceCache };