@savvy-web/silk-effects 10.0.1 → 10.2.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.
@@ -1,10 +1,9 @@
1
- import { parseKitMarkdown } from "../utils/markdown-emit.js";
1
+ import { parseKitMarkdownBody } from "../utils/markdown-emit.js";
2
2
  import { contentStructure } from "../rules/content-structure.js";
3
3
  import { dependencyTableFormat } from "../rules/dependency-table-format.js";
4
4
  import { headingHierarchy } from "../rules/heading-hierarchy.js";
5
5
  import { requiredSections } from "../rules/required-sections.js";
6
6
  import { uncategorizedContent } from "../rules/uncategorized-content.js";
7
- import { stripFrontmatter } from "../utils/strip-frontmatter.js";
8
7
  import { readFileSync, readdirSync } from "node:fs";
9
8
  import { join } from "node:path";
10
9
 
@@ -141,7 +140,7 @@ var ChangesetLinter = class ChangesetLinter {
141
140
  * @returns Array of {@link LintMessage} diagnostics (empty if the content is valid)
142
141
  */
143
142
  static validateContent(content, filePath = "<input>") {
144
- const root = parseKitMarkdown(stripFrontmatter(content));
143
+ const root = parseKitMarkdownBody(content);
145
144
  return RULES.flatMap((rule) => rule(root).map((finding) => ({
146
145
  file: filePath,
147
146
  rule: finding.rule,
@@ -1,5 +1,5 @@
1
1
  import { Result } from "effect";
2
- import { Code, Markdown, MarkdownTransform, Mdast } from "@effected/markdown";
2
+ import { Code, Markdown, MarkdownParseOptions, MarkdownTransform, Mdast, Root } from "@effected/markdown";
3
3
 
4
4
  // src/changesets/utils/markdown-emit.ts
5
5
  /**
@@ -65,6 +65,31 @@ function parseKitMarkdown(text) {
65
65
  const parsed = unwrap(Markdown.parseResult(text), "markdown parse: canonical parse failed");
66
66
  return unwrap(Mdast.fromMdastResult(Mdast.toMdast(parsed)), "markdown parse: fidelity strip failed");
67
67
  }
68
+ const FRONTMATTER_CAPTURE = MarkdownParseOptions.make({ frontmatter: true });
69
+ /**
70
+ * Parse a changeset file into a kit root with its frontmatter block removed.
71
+ *
72
+ * @remarks
73
+ * Parses with the kit's frontmatter capture on, so a `---` block at offset 0
74
+ * is recognized whatever its line endings (CRLF included), then drops the
75
+ * `frontmatter` head node. The text is never sliced, so every remaining
76
+ * node's position is relative to the whole file — the lines a linter reports
77
+ * match the lines an editor shows. A fence pair later in the document is not
78
+ * frontmatter and parses as ordinary content.
79
+ *
80
+ * @param text - The raw changeset source, frontmatter included
81
+ * @returns The parsed kit root, without its frontmatter node
82
+ *
83
+ * @internal
84
+ */
85
+ function parseKitMarkdownBody(text) {
86
+ const parsed = unwrap(Markdown.parseResult(text, FRONTMATTER_CAPTURE), "markdown parse: canonical parse failed");
87
+ const body = new Root({
88
+ children: parsed.children.filter((node) => node.type !== "frontmatter"),
89
+ position: parsed.position
90
+ });
91
+ return unwrap(Mdast.fromMdastResult(Mdast.toMdast(body)), "markdown parse: fidelity strip failed");
92
+ }
68
93
  /**
69
94
  * Serialize a kit root, or a single kit block node, to canonical markdown.
70
95
  *
@@ -81,4 +106,4 @@ function emitKitMarkdown(node) {
81
106
  return unwrap(Markdown.stringifyResult(ensureFencedCode(node)), "markdown emit: canonical stringify failed");
82
107
  }
83
108
 
84
- export { emitKitMarkdown, parseKitMarkdown };
109
+ export { emitKitMarkdown, parseKitMarkdown, parseKitMarkdownBody };
@@ -1,22 +1,29 @@
1
1
  import { readCache, writeCache } from "./cache.js";
2
- import { Effect } from "effect";
2
+ import { Effect, Schema } from "effect";
3
3
  import { resolve } from "node:path";
4
4
  import { Git } from "@effected/git";
5
- import { ChildProcess, ChildProcessSpawner } from "effect/process";
5
+ import { Run } from "@effected/commands";
6
+ import { ChildProcess } from "effect/process";
6
7
 
7
8
  // src/commitlint/hook/diagnostics/open-issues.ts
8
9
  /**
9
10
  * Open-issue lookup via gh CLI, cached on disk.
10
11
  *
11
12
  * @remarks
12
- * The `gh` invocations are not git, so they stay hand-rolled — the spawn
13
- * mechanism moved from promisified `node:child_process.execFile` onto
14
- * `effect/process` `ChildProcess`. Any failure (gh missing, not
15
- * logged in, no repo, malformed JSON) degrades to `null`, preserving the
16
- * never-fails contract of the v3 implementation.
13
+ * The `gh` invocations run through `@effected/commands`' `Run`: the repo
14
+ * lookup is `Run.text`, the issue list is `Run.json` decoded against
15
+ * {@link OpenIssue}. Any failure (gh missing, not logged in, no repo, a
16
+ * non-zero exit, output that is not JSON or not an issue list) degrades to
17
+ * `null` and caches nothing, preserving the never-fails contract.
17
18
  *
18
19
  * @internal
19
20
  */
21
+ /** One open issue as `gh issue list --json number,title` prints it. */
22
+ const OpenIssue = Schema.Struct({
23
+ number: Schema.Number,
24
+ title: Schema.String
25
+ });
26
+ const OpenIssues = Schema.Array(OpenIssue);
20
27
  const ISSUES_CACHE_TTL_SECONDS = 600;
21
28
  /** Relative path under the project root where the open-issues cache lives. */
22
29
  const ISSUES_CACHE_RELATIVE_PATH = ".claude/cache/issues.json";
@@ -43,17 +50,16 @@ function readOpenIssuesFromCache(cachePath, ttlSeconds = 600) {
43
50
  }
44
51
  function fetchAndCacheOpenIssues(cachePath) {
45
52
  return Effect.gen(function* () {
46
- const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
47
- const repo = (yield* spawner.string(ChildProcess.make("gh", [
53
+ const repo = yield* Run.text(ChildProcess.make("gh", [
48
54
  "repo",
49
55
  "view",
50
56
  "--json",
51
57
  "nameWithOwner",
52
58
  "-q",
53
59
  ".nameWithOwner"
54
- ]))).trim();
60
+ ]));
55
61
  if (!repo) return null;
56
- const listStdout = yield* spawner.string(ChildProcess.make("gh", [
62
+ const parsed = [...yield* Run.json(ChildProcess.make("gh", [
57
63
  "issue",
58
64
  "list",
59
65
  "--repo",
@@ -64,8 +70,7 @@ function fetchAndCacheOpenIssues(cachePath) {
64
70
  "20",
65
71
  "--json",
66
72
  "number,title"
67
- ]));
68
- const parsed = yield* Effect.try(() => JSON.parse(listStdout));
73
+ ]), OpenIssues)];
69
74
  yield* writeCache(cachePath, parsed);
70
75
  return parsed;
71
76
  }).pipe(Effect.orElseSucceed(() => null));
@@ -78,4 +83,4 @@ function readOrFetchOpenIssues(cachePath) {
78
83
  });
79
84
  }
80
85
 
81
- export { ISSUES_CACHE_RELATIVE_PATH, fetchAndCacheOpenIssues, readOpenIssuesFromCache, readOrFetchOpenIssues, resolveIssuesCachePath };
86
+ export { ISSUES_CACHE_RELATIVE_PATH, OpenIssue, fetchAndCacheOpenIssues, readOpenIssuesFromCache, readOrFetchOpenIssues, resolveIssuesCachePath };
@@ -1,62 +1,35 @@
1
- import { existsSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { readFile } from "node:fs/promises";
1
+ import { Result } from "effect";
2
+ import { detectPackageManagerSync } from "@effected/workspaces";
3
+ import { nodeSyncOps } from "@effected/workspaces/node-sync";
4
4
 
5
5
  // src/commitlint/hook/diagnostics/package-manager.ts
6
6
  /**
7
7
  * Package manager detection for the commit-time hooks.
8
8
  *
9
- * Mirrors the husky hook detection: prefer `package.json#devEngines.packageManager`
10
- * (the first entry when it is an array), then the legacy `packageManager` field,
11
- * then lockfile presence in priority order pnpm \> yarn \> bun \> npm.
9
+ * Delegates to `@effected/workspaces`' `detectPackageManagerSync` — the same
10
+ * detector `Lint`'s `Command.detectPackageManager` runs — so silk-effects has
11
+ * one answer to "which manager owns this root". The kit refuses to guess when
12
+ * no evidence matches (and fails on an unreadable root `package.json`); the
13
+ * hook hints still need a command to print, so those cases fall back to `npm`.
12
14
  *
13
15
  * @internal
14
16
  */
15
- const VALID_PMS = /* @__PURE__ */ new Set([
16
- "pnpm",
17
- "yarn",
18
- "bun",
19
- "npm"
20
- ]);
21
- const toPackageManager = (name) => typeof name === "string" && VALID_PMS.has(name) ? name : null;
22
- const devEnginesName = (manifest) => {
23
- const devEngines = manifest.devEngines;
24
- if (typeof devEngines !== "object" || devEngines === null) return void 0;
25
- const declared = devEngines.packageManager;
26
- const first = Array.isArray(declared) ? declared[0] : declared;
27
- return typeof first === "object" && first !== null ? first.name : void 0;
28
- };
29
- function parsePackageManagerField(packageJsonContent) {
30
- let parsed;
31
- try {
32
- parsed = JSON.parse(packageJsonContent);
33
- } catch {
34
- return null;
35
- }
36
- if (typeof parsed !== "object" || parsed === null) return null;
37
- const manifest = parsed;
38
- const fromDevEngines = toPackageManager(devEnginesName(manifest));
39
- if (fromDevEngines !== null) return fromDevEngines;
40
- const field = manifest.packageManager;
41
- if (typeof field !== "string" || field.length === 0) return null;
42
- return toPackageManager(field.split("@")[0]);
43
- }
44
- function detectFromLockfiles(presence) {
45
- if (presence.pnpm) return "pnpm";
46
- if (presence.yarn) return "yarn";
47
- if (presence.bun) return "bun";
48
- return "npm";
49
- }
17
+ /**
18
+ * Detect the package manager owning `root`, falling back to `npm`.
19
+ *
20
+ * @remarks
21
+ * Precedence is the kit's (`PackageManagerDetector`): `pnpm-workspace.yaml`,
22
+ * then a bun or yarn lockfile only when a manifest field also names that
23
+ * manager, then `package.json#workspaces` (npm), `pnpm-lock.yaml`,
24
+ * `package-lock.json`, and finally `devEngines.packageManager` over the
25
+ * top-level `packageManager` field. `root` is probed as given, without ascent.
26
+ *
27
+ * @param root - The directory to probe
28
+ * @returns The detected manager, or `"npm"` when none can be attributed
29
+ */
50
30
  async function detectPackageManager(root) {
51
- try {
52
- const fromField = parsePackageManagerField(await readFile(join(root, "package.json"), "utf8"));
53
- if (fromField !== null) return fromField;
54
- } catch {}
55
- return detectFromLockfiles({
56
- pnpm: existsSync(join(root, "pnpm-lock.yaml")),
57
- yarn: existsSync(join(root, "yarn.lock")),
58
- bun: existsSync(join(root, "bun.lock"))
59
- });
31
+ const detected = detectPackageManagerSync(root, nodeSyncOps);
32
+ return Result.isSuccess(detected) ? detected.success.name : "npm";
60
33
  }
61
34
 
62
- export { detectFromLockfiles, detectPackageManager, parsePackageManagerField };
35
+ export { detectPackageManager };
@@ -18,7 +18,7 @@ import { inferTicketId, readBranchInfo } from "./hook/diagnostics/branch.js";
18
18
  import { readCache, writeCache } from "./hook/diagnostics/cache.js";
19
19
  import { parseHuskyConfigPath, readCommitlintConfigPath } from "./hook/diagnostics/commitlint-config.js";
20
20
  import { ISSUES_CACHE_RELATIVE_PATH, fetchAndCacheOpenIssues, readOpenIssuesFromCache, readOrFetchOpenIssues, resolveIssuesCachePath } from "./hook/diagnostics/open-issues.js";
21
- import { detectFromLockfiles, detectPackageManager, parsePackageManagerField } from "./hook/diagnostics/package-manager.js";
21
+ import { detectPackageManager } from "./hook/diagnostics/package-manager.js";
22
22
  import { buildSigningDiagnostic, parseGpgKeyExpiry, readSigningDiagnostic } from "./hook/diagnostics/signing.js";
23
23
  import { closesTrailerRule, hasClosingTrailer } from "./hook/rules/closes-trailer.js";
24
24
  import { forbiddenContentRule } from "./hook/rules/forbidden-content.js";
@@ -69,7 +69,6 @@ var commitlint_exports = /* @__PURE__ */ __exportAll({
69
69
  default: () => CommitlintConfig,
70
70
  defaultPromptConfig: () => defaultPromptConfig,
71
71
  detectDCO: () => detectDCO,
72
- detectFromLockfiles: () => detectFromLockfiles,
73
72
  detectPackageManager: () => detectPackageManager,
74
73
  detectScopes: () => detectScopes,
75
74
  emojiPromptConfig: () => emojiPromptConfig,
@@ -84,7 +83,6 @@ var commitlint_exports = /* @__PURE__ */ __exportAll({
84
83
  parseBashCommand: () => parseBashCommand,
85
84
  parseGpgKeyExpiry: () => parseGpgKeyExpiry,
86
85
  parseHuskyConfigPath: () => parseHuskyConfigPath,
87
- parsePackageManagerField: () => parsePackageManagerField,
88
86
  partitionHits: () => partitionHits,
89
87
  planLeakageRule: () => planLeakageRule,
90
88
  postToolUseAdvise: () => postToolUseAdvise,
@@ -141,4 +139,4 @@ var CommitlintConfig = class {
141
139
  constructor() {}
142
140
  };
143
141
 
144
- export { COMMIT_TYPES, COMMIT_TYPE_DEFINITIONS, CommitlintConfig, CommitlintConfig as default, DCO_SIGNOFF_TEXT, ERROR_EXPLANATIONS, ERROR_SUGGESTIONS, HookSilencer, ISSUES_CACHE_RELATIVE_PATH, PostToolUseEnvelope, PreToolUseEnvelope, SessionStartEnvelope, TDD_SCOPE_PATTERN, TDD_STATES, TYPE_EMOJIS, TYPE_EMOJIS_UNICODE, buildSigningDiagnostic, closesTrailerRule, commitlint_exports, createPromptConfig, createTypeEnum, defaultPromptConfig, detectDCO, detectFromLockfiles, detectPackageManager, detectScopes, emojiPromptConfig, fetchAndCacheOpenIssues, forbiddenContentRule, format, getExplanation, getSuggestion, getTypeEmoji, hasClosingTrailer, inferTicketId, parseBashCommand, parseGpgKeyExpiry, parseHuskyConfigPath, parsePackageManagerField, partitionHits, planLeakageRule, postToolUseAdvise, preToolUseAdvise, preToolUseAllow, preToolUseDeny, preToolUseSilent, prompter, readBranchInfo, readCache, readCommitlintConfigPath, readOpenIssuesFromCache, readOrFetchOpenIssues, readSigningDiagnostic, resolveIssuesCachePath, sessionStartContext, signingFlagConflictRule, softWrapRule, staticConfig, verbosityRule, writeCache };
142
+ export { COMMIT_TYPES, COMMIT_TYPE_DEFINITIONS, CommitlintConfig, CommitlintConfig as default, DCO_SIGNOFF_TEXT, ERROR_EXPLANATIONS, ERROR_SUGGESTIONS, HookSilencer, ISSUES_CACHE_RELATIVE_PATH, PostToolUseEnvelope, PreToolUseEnvelope, SessionStartEnvelope, TDD_SCOPE_PATTERN, TDD_STATES, TYPE_EMOJIS, TYPE_EMOJIS_UNICODE, buildSigningDiagnostic, closesTrailerRule, commitlint_exports, createPromptConfig, createTypeEnum, defaultPromptConfig, detectDCO, detectPackageManager, detectScopes, emojiPromptConfig, fetchAndCacheOpenIssues, forbiddenContentRule, format, getExplanation, getSuggestion, getTypeEmoji, hasClosingTrailer, inferTicketId, parseBashCommand, parseGpgKeyExpiry, parseHuskyConfigPath, partitionHits, planLeakageRule, postToolUseAdvise, preToolUseAdvise, preToolUseAllow, preToolUseDeny, preToolUseSilent, prompter, readBranchInfo, readCache, readCommitlintConfigPath, readOpenIssuesFromCache, readOrFetchOpenIssues, readSigningDiagnostic, resolveIssuesCachePath, sessionStartContext, signingFlagConflictRule, softWrapRule, staticConfig, verbosityRule, writeCache };
package/index.d.ts CHANGED
@@ -975,8 +975,8 @@ interface LintMessage {
975
975
  * Line number where the issue was detected (1-based).
976
976
  *
977
977
  * @remarks
978
- * Line numbers are relative to the content after YAML frontmatter
979
- * stripping (the blank line after the closing `---` counts as line 1).
978
+ * Line numbers are relative to the whole file, frontmatter block
979
+ * included, so they match the lines an editor shows.
980
980
  */
981
981
  line: number;
982
982
  /**
@@ -5456,10 +5456,13 @@ declare function readCache<T>(path: string, ttlSeconds: number): Effect.Effect<T
5456
5456
  declare function writeCache<T>(path: string, data: T, when?: Date): Effect.Effect<void>;
5457
5457
  declare function parseHuskyConfigPath(huskyContent: string, root: string): string | null;
5458
5458
  declare function readCommitlintConfigPath(root: string): Promise<string | null>;
5459
- interface OpenIssue {
5460
- number: number;
5461
- title: string;
5462
- }
5459
+ /** One open issue as `gh issue list --json number,title` prints it. */
5460
+ declare const OpenIssue: Schema.Struct<{
5461
+ readonly number: Schema.Number;
5462
+ readonly title: Schema.String;
5463
+ }>;
5464
+ /** The decoded {@link (OpenIssue:variable)}. */
5465
+ type OpenIssue = typeof OpenIssue.Type;
5463
5466
  declare const ISSUES_CACHE_TTL_SECONDS = 600;
5464
5467
  /** Relative path under the project root where the open-issues cache lives. */
5465
5468
  declare const ISSUES_CACHE_RELATIVE_PATH = ".claude/cache/issues.json";
@@ -5480,13 +5483,19 @@ declare function readOpenIssuesFromCache(cachePath: string, ttlSeconds?: number)
5480
5483
  declare function fetchAndCacheOpenIssues(cachePath: string): Effect.Effect<OpenIssue[] | null, never, ChildProcessSpawner.ChildProcessSpawner>;
5481
5484
  declare function readOrFetchOpenIssues(cachePath: string): Effect.Effect<OpenIssue[] | null, never, ChildProcessSpawner.ChildProcessSpawner>;
5482
5485
  type PackageManager$1 = "pnpm" | "yarn" | "bun" | "npm";
5483
- interface LockfilePresence {
5484
- pnpm: boolean;
5485
- yarn: boolean;
5486
- bun: boolean;
5487
- }
5488
- declare function parsePackageManagerField(packageJsonContent: string): PackageManager$1 | null;
5489
- declare function detectFromLockfiles(presence: LockfilePresence): PackageManager$1;
5486
+ /**
5487
+ * Detect the package manager owning `root`, falling back to `npm`.
5488
+ *
5489
+ * @remarks
5490
+ * Precedence is the kit's (`PackageManagerDetector`): `pnpm-workspace.yaml`,
5491
+ * then a bun or yarn lockfile only when a manifest field also names that
5492
+ * manager, then `package.json#workspaces` (npm), `pnpm-lock.yaml`,
5493
+ * `package-lock.json`, and finally `devEngines.packageManager` over the
5494
+ * top-level `packageManager` field. `root` is probed as given, without ascent.
5495
+ *
5496
+ * @param root - The directory to probe
5497
+ * @returns The detected manager, or `"npm"` when none can be attributed
5498
+ */
5490
5499
  declare function detectPackageManager(root: string): Promise<PackageManager$1>;
5491
5500
  interface SigningDiagnostic {
5492
5501
  format: "gpg" | "ssh" | "none";
@@ -5591,7 +5600,7 @@ declare const VERBOSITY_LINE_THRESHOLD = 12;
5591
5600
  declare const VERBOSITY_WORD_THRESHOLD = 150;
5592
5601
  declare const verbosityRule: Rule<VerbosityInput, never>;
5593
5602
  declare namespace index_d_exports$1 {
5594
- export { BranchInfo, COMMIT_TYPES, COMMIT_TYPE_DEFINITIONS, ClosesTrailerCtx, ClosesTrailerInput, CommitType, CommitTypeDefinition, CommitlintConfig, CommitlintPlugin, CommitlintUserConfig, ConfigOptions, DCO_SIGNOFF_TEXT, DEFAULT_BODY_MAX_LINE_LENGTH, ERROR_EXPLANATIONS, ERROR_SUGGESTIONS, ForbiddenContentInput, FormatterResult, HookOutput, RuleSeverity$1 as HookRuleSeverity, HookSilencer, ISSUES_CACHE_RELATIVE_PATH, ISSUES_CACHE_TTL_SECONDS, Inquirer, LintOutcome, LockfilePresence, OpenIssue, PackageManager$1 as PackageManager, ParsedCommit, ParsedKind, PlanLeakageInput, PostToolUseEnvelope, PostToolUseEnvelope as PostToolUseEnvelopeType, PreToolUseEnvelope, PreToolUseEnvelope as PreToolUseEnvelopeType, PromptConfig, PromptConfigOptions, PromptQuestion, PromptSettings, PrompterOptions, Question, RawSigningInputs, ReleaseFormat, ResolvedPromptConfig, Rule, RuleApplicability, RuleConfigTuple, RuleHit, RuleResult, RuleSeverity, RulesConfig, SessionStartEnvelope, SessionStartEnvelope as SessionStartEnvelopeType, SigningDiagnostic, SigningFlagCtx, SigningFlagInput, SoftWrapInput, TDD_SCOPE_PATTERN, TDD_STATES, TYPE_EMOJIS, TYPE_EMOJIS_UNICODE, TypeEnumEntry, VERBOSITY_LINE_THRESHOLD, VERBOSITY_WORD_THRESHOLD, VerbosityInput, buildSigningDiagnostic, closesTrailerRule, createPromptConfig, createTypeEnum, CommitlintConfig as default, defaultPromptConfig, detectDCO, detectFromLockfiles, detectPackageManager, detectScopes, emojiPromptConfig, fetchAndCacheOpenIssues, forbiddenContentRule, format, getExplanation, getSuggestion, getTypeEmoji, hasClosingTrailer, inferTicketId, parseBashCommand, parseGpgKeyExpiry, parseHuskyConfigPath, parsePackageManagerField, partitionHits, planLeakageRule, postToolUseAdvise, preToolUseAdvise, preToolUseAllow, preToolUseDeny, preToolUseSilent, prompter, readBranchInfo, readCache, readCommitlintConfigPath, readOpenIssuesFromCache, readOrFetchOpenIssues, readSigningDiagnostic, resolveIssuesCachePath, sessionStartContext, signingFlagConflictRule, softWrapRule, staticConfig, verbosityRule, writeCache };
5603
+ export { BranchInfo, COMMIT_TYPES, COMMIT_TYPE_DEFINITIONS, ClosesTrailerCtx, ClosesTrailerInput, CommitType, CommitTypeDefinition, CommitlintConfig, CommitlintPlugin, CommitlintUserConfig, ConfigOptions, DCO_SIGNOFF_TEXT, DEFAULT_BODY_MAX_LINE_LENGTH, ERROR_EXPLANATIONS, ERROR_SUGGESTIONS, ForbiddenContentInput, FormatterResult, HookOutput, RuleSeverity$1 as HookRuleSeverity, HookSilencer, ISSUES_CACHE_RELATIVE_PATH, ISSUES_CACHE_TTL_SECONDS, Inquirer, LintOutcome, OpenIssue, PackageManager$1 as PackageManager, ParsedCommit, ParsedKind, PlanLeakageInput, PostToolUseEnvelope, PostToolUseEnvelope as PostToolUseEnvelopeType, PreToolUseEnvelope, PreToolUseEnvelope as PreToolUseEnvelopeType, PromptConfig, PromptConfigOptions, PromptQuestion, PromptSettings, PrompterOptions, Question, RawSigningInputs, ReleaseFormat, ResolvedPromptConfig, Rule, RuleApplicability, RuleConfigTuple, RuleHit, RuleResult, RuleSeverity, RulesConfig, SessionStartEnvelope, SessionStartEnvelope as SessionStartEnvelopeType, SigningDiagnostic, SigningFlagCtx, SigningFlagInput, SoftWrapInput, TDD_SCOPE_PATTERN, TDD_STATES, TYPE_EMOJIS, TYPE_EMOJIS_UNICODE, TypeEnumEntry, VERBOSITY_LINE_THRESHOLD, VERBOSITY_WORD_THRESHOLD, VerbosityInput, buildSigningDiagnostic, closesTrailerRule, createPromptConfig, createTypeEnum, CommitlintConfig as default, defaultPromptConfig, detectDCO, detectPackageManager, detectScopes, emojiPromptConfig, fetchAndCacheOpenIssues, forbiddenContentRule, format, getExplanation, getSuggestion, getTypeEmoji, hasClosingTrailer, inferTicketId, parseBashCommand, parseGpgKeyExpiry, parseHuskyConfigPath, partitionHits, planLeakageRule, postToolUseAdvise, preToolUseAdvise, preToolUseAllow, preToolUseDeny, preToolUseSilent, prompter, readBranchInfo, readCache, readCommitlintConfigPath, readOpenIssuesFromCache, readOrFetchOpenIssues, readSigningDiagnostic, resolveIssuesCachePath, sessionStartContext, signingFlagConflictRule, softWrapRule, staticConfig, verbosityRule, writeCache };
5595
5604
  }
5596
5605
  /**
5597
5606
  * Dynamic commitlint configuration factory.
@@ -6939,6 +6948,9 @@ declare class Command {
6939
6948
  private static cachedPackageManager;
6940
6949
  /** Cached project root path */
6941
6950
  private static cachedRoot;
6951
+ /** Memoizing tool discovery, created lazily and dropped by `clearCache` */
6952
+ private static discovery;
6953
+ private static getDiscovery;
6942
6954
  /**
6943
6955
  * Find the project root directory by walking up from `cwd`.
6944
6956
  *
@@ -6963,9 +6975,8 @@ declare class Command {
6963
6975
  /**
6964
6976
  * Detect the package manager declared by the root package.json.
6965
6977
  *
6966
- * Reads `devEngines.packageManager.name` (the first entry when it is an array), then the
6967
- * legacy `packageManager` field (e.g., `pnpm\@9.0.0`), then lockfile presence in priority
6968
- * order pnpm, yarn, bun. Falls back to "npm" when none of them names a manager.
6978
+ * Delegates to the kit's `detectPackageManagerSync` (manifest fields, then workspace and
6979
+ * lockfile markers) at `cwd` without walking up. Falls back to "npm" when none names a manager.
6969
6980
  *
6970
6981
  * @param cwd - Directory to search for package.json (defaults to `Command.findRoot()`)
6971
6982
  * @returns The detected package manager
@@ -6986,7 +6997,7 @@ declare class Command {
6986
6997
  * @example
6987
6998
  * ```typescript
6988
6999
  * Command.getExecPrefix('pnpm'); // ['pnpm', 'exec']
6989
- * Command.getExecPrefix('npm'); // ['npx', '--no']
7000
+ * Command.getExecPrefix('npm'); // ['npx', '--no', '--']
6990
7001
  * Command.getExecPrefix('yarn'); // ['yarn', 'exec']
6991
7002
  * Command.getExecPrefix('bun'); // ['bun', 'x', "--no-install"]
6992
7003
  * ```
@@ -7061,6 +7072,8 @@ declare class Command {
7061
7072
  /**
7062
7073
  * Execute a command and return its output.
7063
7074
  *
7075
+ * @deprecated No in-repo consumers; will be removed in a future major.
7076
+ *
7064
7077
  * @param command - The command to execute
7065
7078
  * @returns The command output as a string (trimmed)
7066
7079
  * @throws If the command fails
@@ -7075,6 +7088,8 @@ declare class Command {
7075
7088
  /**
7076
7089
  * Execute a command silently (ignore output).
7077
7090
  *
7091
+ * @deprecated No in-repo consumers; will be removed in a future major.
7092
+ *
7078
7093
  * @param command - The command to execute
7079
7094
  * @returns `true` if successful, `false` if failed
7080
7095
  */
@@ -8015,6 +8030,15 @@ declare class ReposLockdown extends ReposLockdown_base {
8015
8030
  */
8016
8031
  static readonly layer: Layer.Layer<ReposLockdown, never, FileSystem.FileSystem | Path.Path>;
8017
8032
  }
8033
+ /**
8034
+ * Default repo name for `add`: the last segment of the remote's repository
8035
+ * path, as `@effected/git`'s `GitRemote.repositoryPath` reads it (one trailing
8036
+ * `/` and one trailing `.git` dropped). Every remote form git accepts is
8037
+ * covered, scp without a slash included — `git@host:repo.git` is `repo`.
8038
+ *
8039
+ * @internal
8040
+ */
8041
+ declare const repoSlug: (url: string) => string;
8018
8042
  /**
8019
8043
  * Minimum age (in milliseconds) a `.lock` file must reach before `sync` will
8020
8044
  * remove it. An ACTIVE git process can legitimately hold
@@ -8145,7 +8169,7 @@ declare class ReposManager extends ReposManager_base {
8145
8169
  static readonly layer: Layer.Layer<ReposManager, never, ReposConfigStore | Git | FileSystem.FileSystem | Path.Path | ReposLockdown>;
8146
8170
  }
8147
8171
  declare namespace index_d_exports$3 {
8148
- export { DriftKind, GitSubmoduleError, GitSubmoduleErrorBase, MANIFEST_PATH, NOTE_LIMIT, NoteNotFoundError, NoteNotFoundErrorBase, REPOS_DIR, RepoDrift, RepoEntry, RepoName, RepoNotFoundError, RepoNotFoundErrorBase, RepoNote, RepoOrientation, RepoStatusEntry, ReposAddResult, ReposConfigError, ReposConfigErrorBase, ReposConfigStore, ReposConfigStoreShape, ReposDeregisterResult, ReposDrift, ReposDriftReport, ReposDriftShape, ReposLockdown, ReposLockdownError, ReposLockdownErrorBase, ReposLockdownShape, ReposManager, ReposManagerShape, ReposManifestFile, ReposNoteResult, ReposPinResult, ReposRemoveResult, ReposRenameResult, ReposRestoreResult, ReposStatusReport, ReposSyncReport, STALE_LOCK_MAX_AGE_MS, resolveModuleDir, submoduleNameFromKey };
8172
+ export { DriftKind, GitSubmoduleError, GitSubmoduleErrorBase, MANIFEST_PATH, NOTE_LIMIT, NoteNotFoundError, NoteNotFoundErrorBase, REPOS_DIR, RepoDrift, RepoEntry, RepoName, RepoNotFoundError, RepoNotFoundErrorBase, RepoNote, RepoOrientation, RepoStatusEntry, ReposAddResult, ReposConfigError, ReposConfigErrorBase, ReposConfigStore, ReposConfigStoreShape, ReposDeregisterResult, ReposDrift, ReposDriftReport, ReposDriftShape, ReposLockdown, ReposLockdownError, ReposLockdownErrorBase, ReposLockdownShape, ReposManager, ReposManagerShape, ReposManifestFile, ReposNoteResult, ReposPinResult, ReposRemoveResult, ReposRenameResult, ReposRestoreResult, ReposStatusReport, ReposSyncReport, STALE_LOCK_MAX_AGE_MS, repoSlug, resolveModuleDir, submoduleNameFromKey };
8149
8173
  }
8150
8174
  /**
8151
8175
  * Strip leading semver range operators from a version string.
@@ -1,7 +1,8 @@
1
1
  import { Command } from "../utils/Command.js";
2
2
  import { Filter } from "../utils/Filter.js";
3
- import { Data, Effect, FileSystem, Path, Schema, Stream } from "effect";
3
+ import { Data, Effect, FileSystem, Option, Path, Schema, Stream } from "effect";
4
4
  import { Git } from "@effected/git";
5
+ import { Walker } from "@effected/walker";
5
6
  import { ChildProcess, ChildProcessSpawner } from "effect/process";
6
7
 
7
8
  // src/lint/handlers/TextFiles.ts
@@ -279,12 +280,8 @@ var TextFiles = class TextFiles {
279
280
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
280
281
  const fs = yield* FileSystem.FileSystem;
281
282
  const pathService = yield* Path.Path;
282
- let cwd = pathService.dirname(path);
283
- while (!(yield* fs.exists(cwd))) {
284
- const parent = pathService.dirname(cwd);
285
- if (parent === cwd) break;
286
- cwd = parent;
287
- }
283
+ const chain = yield* Walker.ascend(pathService.dirname(path));
284
+ const cwd = Option.getOrElse(yield* Walker.firstMatch(chain, (dir) => fs.exists(dir)), () => chain[chain.length - 1] ?? pathService.dirname(path));
288
285
  const relative = pathService.relative(cwd, path).split(pathService.sep).join("/");
289
286
  const command = ChildProcess.make("git", [
290
287
  "cat-file",
@@ -1,7 +1,11 @@
1
- import { detectFromLockfiles, parsePackageManagerField } from "../../commitlint/hook/diagnostics/package-manager.js";
2
- import { existsSync, readFileSync } from "node:fs";
1
+ import { Result } from "effect";
2
+ import { existsSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
+ import { detectPackageManagerSync } from "@effected/workspaces";
5
+ import { LocalExec, Tool, makeToolDiscoverySync } from "@effected/commands";
6
+ import { nodeSyncOps } from "@effected/workspaces/node-sync";
4
7
  import { execSync } from "node:child_process";
8
+ import { nodeSpawnSync } from "@effected/commands/node-sync";
5
9
 
6
10
  // src/lint/utils/Command.ts
7
11
  /**
@@ -46,6 +50,18 @@ var Command = class Command {
46
50
  static cachedPackageManager = null;
47
51
  /** Cached project root path */
48
52
  static cachedRoot = null;
53
+ /** Memoizing tool discovery, created lazily and dropped by `clearCache` */
54
+ static discovery = null;
55
+ static getDiscovery() {
56
+ if (Command.discovery === null) {
57
+ const context = LocalExec.contextFor(Command.detectPackageManager(), { directory: Command.findRoot() });
58
+ Command.discovery = makeToolDiscoverySync({
59
+ spawn: nodeSpawnSync,
60
+ context
61
+ });
62
+ }
63
+ return Command.discovery;
64
+ }
49
65
  /**
50
66
  * Find the project root directory by walking up from `cwd`.
51
67
  *
@@ -84,9 +100,8 @@ var Command = class Command {
84
100
  /**
85
101
  * Detect the package manager declared by the root package.json.
86
102
  *
87
- * Reads `devEngines.packageManager.name` (the first entry when it is an array), then the
88
- * legacy `packageManager` field (e.g., `pnpm\@9.0.0`), then lockfile presence in priority
89
- * order pnpm, yarn, bun. Falls back to "npm" when none of them names a manager.
103
+ * Delegates to the kit's `detectPackageManagerSync` (manifest fields, then workspace and
104
+ * lockfile markers) at `cwd` without walking up. Falls back to "npm" when none names a manager.
90
105
  *
91
106
  * @param cwd - Directory to search for package.json (defaults to `Command.findRoot()`)
92
107
  * @returns The detected package manager
@@ -99,16 +114,8 @@ var Command = class Command {
99
114
  */
100
115
  static detectPackageManager(cwd = Command.findRoot()) {
101
116
  if (Command.cachedPackageManager !== null) return Command.cachedPackageManager;
102
- const packageJsonPath = join(cwd, "package.json");
103
- let declared = null;
104
- try {
105
- declared = parsePackageManagerField(readFileSync(packageJsonPath, "utf-8"));
106
- } catch {}
107
- Command.cachedPackageManager = declared ?? detectFromLockfiles({
108
- pnpm: existsSync(join(cwd, "pnpm-lock.yaml")),
109
- yarn: existsSync(join(cwd, "yarn.lock")),
110
- bun: existsSync(join(cwd, "bun.lock"))
111
- });
117
+ const detected = detectPackageManagerSync(cwd, nodeSyncOps);
118
+ Command.cachedPackageManager = Result.isSuccess(detected) ? detected.success.name : "npm";
112
119
  return Command.cachedPackageManager;
113
120
  }
114
121
  /**
@@ -120,22 +127,13 @@ var Command = class Command {
120
127
  * @example
121
128
  * ```typescript
122
129
  * Command.getExecPrefix('pnpm'); // ['pnpm', 'exec']
123
- * Command.getExecPrefix('npm'); // ['npx', '--no']
130
+ * Command.getExecPrefix('npm'); // ['npx', '--no', '--']
124
131
  * Command.getExecPrefix('yarn'); // ['yarn', 'exec']
125
132
  * Command.getExecPrefix('bun'); // ['bun', 'x', "--no-install"]
126
133
  * ```
127
134
  */
128
135
  static getExecPrefix(packageManager) {
129
- switch (packageManager) {
130
- case "pnpm": return ["pnpm", "exec"];
131
- case "yarn": return ["yarn", "exec"];
132
- case "bun": return [
133
- "bun",
134
- "x",
135
- "--no-install"
136
- ];
137
- default: return ["npx", "--no"];
138
- }
136
+ return [...LocalExec.contextFor(packageManager).prefix];
139
137
  }
140
138
  /**
141
139
  * Clear the cached package manager and project root detection.
@@ -144,6 +142,8 @@ var Command = class Command {
144
142
  static clearCache() {
145
143
  Command.cachedPackageManager = null;
146
144
  Command.cachedRoot = null;
145
+ Command.discovery?.invalidateAll();
146
+ Command.discovery = null;
147
147
  }
148
148
  /**
149
149
  * Check if a command is available in the system PATH.
@@ -160,12 +160,7 @@ var Command = class Command {
160
160
  */
161
161
  static isAvailable(command) {
162
162
  validateCommandName(command);
163
- try {
164
- execSync(`command -v ${command}`, { stdio: "ignore" });
165
- return true;
166
- } catch {
167
- return false;
168
- }
163
+ return Command.getDiscovery().isAvailable(Tool.named(command, { source: "global" }));
169
164
  }
170
165
  /**
171
166
  * Find a tool, checking global installation first, then the project's package manager.
@@ -188,26 +183,18 @@ var Command = class Command {
188
183
  */
189
184
  static findTool(tool) {
190
185
  validateCommandName(tool);
191
- if (Command.isAvailable(tool)) return {
192
- available: true,
193
- command: tool,
194
- source: "global"
195
- };
196
- const pm = Command.detectPackageManager();
197
- const execCmd = [...Command.getExecPrefix(pm), tool].join(" ");
198
- try {
199
- execSync(`${execCmd} --version`, { stdio: "ignore" });
200
- return {
201
- available: true,
202
- command: execCmd,
203
- source: pm
204
- };
205
- } catch {}
206
- return {
186
+ const resolved = Command.getDiscovery().resolve(Tool.named(tool, { prefer: "global" }));
187
+ if (Result.isFailure(resolved)) return {
207
188
  available: false,
208
189
  command: void 0,
209
190
  source: void 0
210
191
  };
192
+ const found = resolved.success;
193
+ return {
194
+ available: true,
195
+ command: found.argv().join(" "),
196
+ source: found.source === "global" ? "global" : Command.detectPackageManager()
197
+ };
211
198
  }
212
199
  /**
213
200
  * Find a tool or throw an error if not available.
@@ -247,6 +234,8 @@ var Command = class Command {
247
234
  /**
248
235
  * Execute a command and return its output.
249
236
  *
237
+ * @deprecated No in-repo consumers; will be removed in a future major.
238
+ *
250
239
  * @param command - The command to execute
251
240
  * @returns The command output as a string (trimmed)
252
241
  * @throws If the command fails
@@ -263,6 +252,8 @@ var Command = class Command {
263
252
  /**
264
253
  * Execute a command silently (ignore output).
265
254
  *
255
+ * @deprecated No in-repo consumers; will be removed in a future major.
256
+ *
266
257
  * @param command - The command to execute
267
258
  * @returns `true` if successful, `false` if failed
268
259
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/silk-effects",
3
- "version": "10.0.1",
3
+ "version": "10.2.0",
4
4
  "private": false,
5
5
  "description": "Shared Effect library for Silk Suite conventions",
6
6
  "homepage": "https://github.com/savvy-web/systems/tree/main/packages/silk-effects",
@@ -38,22 +38,22 @@
38
38
  "@changesets/pre": "^3.0.0",
39
39
  "@changesets/read": "^1.0.1",
40
40
  "@effected/github-references": "^0.11.0",
41
- "@effected/glob": "^0.14.0",
42
- "@effected/jsonc": "^0.20.0",
41
+ "@effected/glob": "^0.15.0",
42
+ "@effected/jsonc": "^0.20.1",
43
43
  "@effected/markdown": "^0.23.0",
44
- "@effected/package-json": "^0.25.2",
44
+ "@effected/package-json": "^0.26.1",
45
45
  "@effected/toml": "^0.16.0",
46
- "@effected/walker": "^0.20.0",
46
+ "@effected/walker": "^0.21.0",
47
47
  "@effected/yaml": "^0.25.1",
48
48
  "@manypkg/get-packages": "^3.1.0",
49
- "@savvy-web/silk-core": "^0.5.1",
49
+ "@savvy-web/silk-core": "^0.5.2",
50
50
  "shell-quote": "^1.12.0"
51
51
  },
52
52
  "peerDependencies": {
53
- "@effected/commands": "^0.15.0",
54
- "@effected/git": "^0.25.0",
53
+ "@effected/commands": "^0.17.0",
54
+ "@effected/git": "^0.26.0",
55
55
  "@effected/templates": "^0.14.0",
56
- "@effected/workspaces": "^0.38.0",
56
+ "@effected/workspaces": "^0.39.0",
57
57
  "effect": "^4.0.0"
58
58
  }
59
59
  }
package/repos/index.js CHANGED
@@ -7,7 +7,7 @@ import { RepoStatusEntry, ReposAddResult, ReposDeregisterResult, ReposNoteResult
7
7
  import { ReposConfigStore } from "./services/config-store.js";
8
8
  import { ReposLockdown, resolveModuleDir } from "./services/lockdown.js";
9
9
  import { ReposDrift, submoduleNameFromKey } from "./services/drift.js";
10
- import { ReposManager, STALE_LOCK_MAX_AGE_MS } from "./services/manager.js";
10
+ import { ReposManager, STALE_LOCK_MAX_AGE_MS, repoSlug } from "./services/manager.js";
11
11
 
12
12
  // src/repos/index.ts
13
13
  var repos_exports = /* @__PURE__ */ __exportAll({
@@ -47,8 +47,9 @@ var repos_exports = /* @__PURE__ */ __exportAll({
47
47
  ReposStatusReport: () => ReposStatusReport,
48
48
  ReposSyncReport: () => ReposSyncReport,
49
49
  STALE_LOCK_MAX_AGE_MS: () => STALE_LOCK_MAX_AGE_MS,
50
+ repoSlug: () => repoSlug,
50
51
  resolveModuleDir: () => resolveModuleDir,
51
52
  submoduleNameFromKey: () => submoduleNameFromKey
52
53
  });
53
54
 
54
- export { DriftKind, GitSubmoduleError, GitSubmoduleErrorBase, MANIFEST_PATH, NoteNotFoundError, NoteNotFoundErrorBase, REPOS_DIR, RepoDrift, RepoEntry, RepoName, RepoNotFoundError, RepoNotFoundErrorBase, RepoNote, RepoOrientation, RepoStatusEntry, ReposAddResult, ReposConfigError, ReposConfigErrorBase, ReposConfigStore, ReposDeregisterResult, ReposDrift, ReposDriftReport, ReposLockdown, ReposLockdownError, ReposLockdownErrorBase, ReposManager, ReposManifestFile, ReposNoteResult, ReposPinResult, ReposRemoveResult, ReposRenameResult, ReposRestoreResult, ReposStatusReport, ReposSyncReport, STALE_LOCK_MAX_AGE_MS, repos_exports, resolveModuleDir, submoduleNameFromKey };
55
+ export { DriftKind, GitSubmoduleError, GitSubmoduleErrorBase, MANIFEST_PATH, NoteNotFoundError, NoteNotFoundErrorBase, REPOS_DIR, RepoDrift, RepoEntry, RepoName, RepoNotFoundError, RepoNotFoundErrorBase, RepoNote, RepoOrientation, RepoStatusEntry, ReposAddResult, ReposConfigError, ReposConfigErrorBase, ReposConfigStore, ReposDeregisterResult, ReposDrift, ReposDriftReport, ReposLockdown, ReposLockdownError, ReposLockdownErrorBase, ReposManager, ReposManifestFile, ReposNoteResult, ReposPinResult, ReposRemoveResult, ReposRenameResult, ReposRestoreResult, ReposStatusReport, ReposSyncReport, STALE_LOCK_MAX_AGE_MS, repoSlug, repos_exports, resolveModuleDir, submoduleNameFromKey };
@@ -5,7 +5,7 @@ import { ReposConfigStore } from "./config-store.js";
5
5
  import { ReposLockdown, resolveModuleDir } from "./lockdown.js";
6
6
  import { submoduleNameFromKey } from "./drift.js";
7
7
  import { Clock, Context, Effect, Exit, FileSystem, Layer, Option, Path, Result, Schema } from "effect";
8
- import { Git, GitConfig, Gitmodules, LsRemoteEntry } from "@effected/git";
8
+ import { Git, GitConfig, GitRemote, Gitmodules, LsRemoteEntry } from "@effected/git";
9
9
  import { createHash } from "node:crypto";
10
10
 
11
11
  // src/repos/services/manager.ts
@@ -15,6 +15,22 @@ import { createHash } from "node:crypto";
15
15
  */
16
16
  const STALE_LOCKS = ["index.lock", "shallow.lock"];
17
17
  /**
18
+ * Default repo name for `add`: the last segment of the remote's repository
19
+ * path, as `@effected/git`'s `GitRemote.repositoryPath` reads it (one trailing
20
+ * `/` and one trailing `.git` dropped). Every remote form git accepts is
21
+ * covered, scp without a slash included — `git@host:repo.git` is `repo`.
22
+ *
23
+ * @internal
24
+ */
25
+ const repoSlug = (url) => {
26
+ const trimmed = url.trim();
27
+ const path = Option.match(GitRemote.parse(trimmed), {
28
+ onNone: () => trimmed,
29
+ onSome: GitRemote.repositoryPath
30
+ });
31
+ return path.split("/").filter((segment) => segment.length > 0).pop() ?? path;
32
+ };
33
+ /**
18
34
  * Minimum age (in milliseconds) a `.lock` file must reach before `sync` will
19
35
  * remove it. An ACTIVE git process can legitimately hold
20
36
  * `index.lock`/`shallow.lock` for the duration of its own run; removing a
@@ -191,11 +207,6 @@ var ReposManager = class extends Context.Service()("@savvy-web/silk-effects/Repo
191
207
  boundaryMarked
192
208
  };
193
209
  });
194
- /** Last path segment of a repo URL, with a trailing `.git` stripped. */
195
- const repoSlug = (url) => {
196
- const last = url.split("/").filter((segment) => segment.length > 0).pop() ?? url;
197
- return last.endsWith(".git") ? last.slice(0, -4) : last;
198
- };
199
210
  /**
200
211
  * Fetch `ref` shallow into a submodule via the kit's `Git.fetchAny`:
201
212
  * tag-form fetch first (tags need the explicit `fetch origin tag <ref>`
@@ -728,4 +739,4 @@ var ReposManager = class extends Context.Service()("@savvy-web/silk-effects/Repo
728
739
  }));
729
740
  };
730
741
 
731
- export { ReposManager, STALE_LOCK_MAX_AGE_MS };
742
+ export { ReposManager, STALE_LOCK_MAX_AGE_MS, repoSlug };
@@ -1,10 +1,35 @@
1
1
  import { ChangesetConfig } from "./ChangesetConfig.js";
2
2
  import { PublishTargetBindingError, ReleaseChannelError, trimTrailingSlashes } from "@savvy-web/silk-core";
3
- import { Effect, FileSystem, Layer } from "effect";
3
+ import { Effect, FileSystem, Layer, Schema } from "effect";
4
4
  import { isAbsolute, join, relative } from "node:path";
5
5
  import { PublishTarget, PublishabilityDetector, WorkspaceDiscovery } from "@effected/workspaces";
6
6
 
7
7
  // src/services/SilkPublishability.ts
8
+ /**
9
+ * Structural decoder for {@link TargetsBinding}.
10
+ *
11
+ * @remarks
12
+ * `releases` is decoded as `unknown` on purpose: an unrecognised channel must surface as a typed
13
+ * `ReleaseChannelError` from `releaseChannelOf` (which names the package and target), not collapse
14
+ * into a missing binding here. Every other field is validated structurally.
15
+ */
16
+ const TargetsBindingSchema = Schema.Struct({
17
+ groups: Schema.Array(Schema.Struct({
18
+ id: Schema.String,
19
+ name: Schema.String,
20
+ dir: Schema.String
21
+ })),
22
+ targets: Schema.Array(Schema.Struct({
23
+ id: Schema.String,
24
+ group: Schema.String,
25
+ name: Schema.String,
26
+ registry: Schema.String,
27
+ releases: Schema.optionalKey(Schema.Unknown),
28
+ provenance: Schema.optionalKey(Schema.Boolean)
29
+ }))
30
+ });
31
+ /** `targets.json` file contents (a JSON string) decoded to a {@link TargetsBinding}. */
32
+ const TargetsBindingFromJson = Schema.fromJsonString(TargetsBindingSchema);
8
33
  const NPM_DEFAULT = "https://registry.npmjs.org/";
9
34
  /**
10
35
  * Default registry endpoints for the well-known target keys. Kept in sync with the
@@ -413,9 +438,6 @@ const readRaw = (fs, packageJsonPath) => fs.readFileString(packageJsonPath).pipe
413
438
  * @since 1.0.0
414
439
  * @public
415
440
  */
416
- const readTargetsBinding = (fs, pkgPath) => fs.readFileString(join(pkgPath, "dist", "prod", "targets.json")).pipe(Effect.flatMap((content) => Effect.try({
417
- try: () => JSON.parse(content),
418
- catch: () => /* @__PURE__ */ new Error("invalid targets.json")
419
- })), Effect.orElseSucceed(() => null));
441
+ const readTargetsBinding = (fs, pkgPath) => fs.readFileString(join(pkgPath, "dist", "prod", "targets.json")).pipe(Effect.flatMap(Schema.decodeUnknownEffect(TargetsBindingFromJson)), Effect.map((binding) => binding), Effect.orElseSucceed(() => null));
420
442
 
421
443
  export { SilkPublishability, readTargetsBinding };
@@ -3,8 +3,8 @@ import { DryRunParseError, NotATurboRepoError, TurboExecError, TurboNotInstalled
3
3
  import { TurboDryRun } from "../schemas/DryRun.js";
4
4
  import { Context, Effect, FileSystem, Layer, Schema } from "effect";
5
5
  import { Git } from "@effected/git";
6
- import { ChildProcess, ChildProcessSpawner } from "effect/process";
7
6
  import { Run, Tool, ToolDiscovery } from "@effected/commands";
7
+ import { ChildProcess, ChildProcessSpawner } from "effect/process";
8
8
 
9
9
  // src/turbo/services/TurboInspector.ts
10
10
  /**
@@ -1,26 +0,0 @@
1
- // src/changesets/utils/strip-frontmatter.ts
2
- /**
3
- * Strip leading YAML frontmatter from a markdown string.
4
- *
5
- * Removes the `---\n...\n---\n` block at the start of the content.
6
- * If no frontmatter is present, the content is returned as-is.
7
- *
8
- * @param content - Raw markdown string potentially containing frontmatter
9
- * @returns The markdown content with frontmatter removed
10
- *
11
- * @example
12
- * ```typescript
13
- * import { stripFrontmatter } from "../utils/strip-frontmatter.js";
14
- *
15
- * const raw = "---\n\"pkg\": minor\n---\n\n## Features\n\n- New API";
16
- * const body = stripFrontmatter(raw);
17
- * // "\n## Features\n\n- New API"
18
- * ```
19
- *
20
- * @internal
21
- */
22
- function stripFrontmatter(content) {
23
- return content.replace(/^---\n[\s\S]*?\n---\n?/, "");
24
- }
25
-
26
- export { stripFrontmatter };