@savvy-web/silk-effects 3.2.5 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -5187,7 +5187,7 @@ interface TokenTypeMap {
5187
5187
  chunkString: 'chunkString';
5188
5188
  }
5189
5189
  //#endregion
5190
- //#region ../../node_modules/.pnpm/markdownlint@0.41.0/node_modules/markdownlint/lib/markdownlint.d.mts
5190
+ //#region ../../node_modules/.pnpm/markdownlint@0.41.1/node_modules/markdownlint/lib/markdownlint.d.mts
5191
5191
  /**
5192
5192
  * Function to implement rule logic.
5193
5193
  */
@@ -5460,7 +5460,7 @@ type Rule$2 = {
5460
5460
  */
5461
5461
  type RuleConfiguration = boolean | any;
5462
5462
  //#endregion
5463
- //#region ../../node_modules/.pnpm/markdownlint@0.41.0/node_modules/markdownlint/lib/exports.d.mts
5463
+ //#region ../../node_modules/.pnpm/markdownlint@0.41.1/node_modules/markdownlint/lib/exports.d.mts
5464
5464
  type Rule$1 = Rule$2;
5465
5465
  //#endregion
5466
5466
  //#region src/changesets/markdownlint/rules/content-structure.d.ts
@@ -7484,11 +7484,16 @@ declare class PnpmWorkspace {
7484
7484
  /**
7485
7485
  * Handler for shell script files.
7486
7486
  *
7487
- * Removes executable bit by default (security best practice).
7487
+ * Removes the executable bit by default: Silk repos invoke shell scripts via
7488
+ * `bash <script>`, so the bit is never needed at runtime, and normalizing the
7489
+ * mode keeps diffs clean. Committed `.sh` files landing as `100644` is
7490
+ * intentional, not mode drift.
7488
7491
  *
7489
7492
  * @remarks
7490
- * By default, excludes `.claude/scripts/` which need to remain executable
7491
- * for lint-staged hooks to work.
7493
+ * The default excludes `.claude/scripts/` as a consumer escape-hatch
7494
+ * convention: a repo that needs a script to stay executable across commits
7495
+ * can place it there (or pass its own `exclude` list). Nothing in Silk itself
7496
+ * requires the directory to exist.
7492
7497
  *
7493
7498
  * @example
7494
7499
  * ```typescript
@@ -8625,7 +8630,7 @@ declare const MARKDOWNLINT_TEMPLATE: {
8625
8630
  readonly fix: true;
8626
8631
  readonly gitignore: true;
8627
8632
  readonly noBanner: true;
8628
- readonly ignores: readonly ["**/.git", "**/node_modules", "**/.cache", "**/coverage", "**/.coverage", "**/dist", "**/CHANGELOG.md", "**/.claude/plans", "**/docs/superpowers", "**/__test__/**/fixtures/**", "**/__fixtures__/**"];
8633
+ readonly ignores: readonly ["**/.git", "**/node_modules", "**/.cache", "**/coverage", "**/.coverage", "**/dist", "**/CHANGELOG.md", "**/.claude/plans", "**/docs/superpowers", "**/__test__/**/fixtures/**", "**/__fixtures__/**", "**/.repos"];
8629
8634
  readonly customRules: readonly ["@savvy-web/silk/changesets/markdownlint"];
8630
8635
  readonly config: {
8631
8636
  readonly default: true;
@@ -8787,6 +8792,341 @@ declare namespace index_d_exports$2 {
8787
8792
  export { BaseHandlerOptions, Biome, BiomeOptions, Command$1 as Command, CreateConfigOptions, DEFAULT_CONFIG_PATH, Filter, HUSKY_HOOK_PATH, Handler, LegacySavvyLintHygieneDef, LintStagedConfig, LintStagedEntry, LintStagedHandler, MARKDOWNLINT_CONFIG, MARKDOWNLINT_CONFIG_PATH, MARKDOWNLINT_SCHEMA, MARKDOWNLINT_TEMPLATE, Markdown, MarkdownOptions, POST_CHECKOUT_HOOK_PATH, POST_COMMIT_HOOK_PATH, POST_MERGE_HOOK_PATH, PackageJson, PackageJsonOptions, PackageManager, PnpmWorkspace, PnpmWorkspaceContent, PnpmWorkspaceOptions, Preset, PresetExtendOptions, PresetType, SavvyLintSectionDef, ShellScripts, ShellScriptsOptions, ToolSearchResult, TypeScript, TypeScriptCompiler, TypeScriptOptions, WorkspacePackageInfo, Yaml, YamlOptions, createConfig, generateManagedContent, getWorkspacePackagePaths, getWorkspacePackages, getWorkspaceRoot, isWorkspacePackagePath, resetWorkspaceCache, savvyLintBlock };
8788
8793
  }
8789
8794
  //#endregion
8795
+ //#region src/repos/constants.d.ts
8796
+ /**
8797
+ * Directory vendored reference repos live under, relative to the repo root.
8798
+ * @public
8799
+ */
8800
+ declare const REPOS_DIR = ".repos";
8801
+ /**
8802
+ * Path of the committed vendored-repos manifest, relative to the repo root.
8803
+ * @public
8804
+ */
8805
+ declare const MANIFEST_PATH = ".repos/config.json";
8806
+ /**
8807
+ * Maximum notes per vendored repo; enforced at write time to force
8808
+ * consolidation into orientation.
8809
+ * @public
8810
+ */
8811
+ declare const NOTE_LIMIT = 10;
8812
+ //#endregion
8813
+ //#region src/repos/errors.d.ts
8814
+ /** @internal */
8815
+ declare const ReposConfigErrorBase: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
8816
+ readonly _tag: "ReposConfigError";
8817
+ } & Readonly<A>;
8818
+ /**
8819
+ * The .repos/config.json manifest is missing, unreadable, or invalid.
8820
+ * @public
8821
+ */
8822
+ declare class ReposConfigError extends ReposConfigErrorBase<{
8823
+ readonly path: string;
8824
+ readonly reason: string;
8825
+ readonly kind: "missing" | "invalid";
8826
+ }> {
8827
+ get message(): string;
8828
+ }
8829
+ /** @internal */
8830
+ declare const GitSubmoduleErrorBase: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
8831
+ readonly _tag: "GitSubmoduleError";
8832
+ } & Readonly<A>;
8833
+ /**
8834
+ * A git submodule operation failed.
8835
+ * @public
8836
+ */
8837
+ declare class GitSubmoduleError extends GitSubmoduleErrorBase<{
8838
+ readonly command: string;
8839
+ readonly cwd: string;
8840
+ readonly reason: string;
8841
+ }> {
8842
+ get message(): string;
8843
+ }
8844
+ /** @internal */
8845
+ declare const RepoNotFoundErrorBase: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
8846
+ readonly _tag: "RepoNotFoundError";
8847
+ } & Readonly<A>;
8848
+ /**
8849
+ * The named repo is not present in the manifest.
8850
+ * @public
8851
+ */
8852
+ declare class RepoNotFoundError extends RepoNotFoundErrorBase<{
8853
+ readonly name: string;
8854
+ }> {
8855
+ get message(): string;
8856
+ }
8857
+ /** @internal */
8858
+ declare const NoteNotFoundErrorBase: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
8859
+ readonly _tag: "NoteNotFoundError";
8860
+ } & Readonly<A>;
8861
+ /**
8862
+ * The note id does not exist on the named repo.
8863
+ * @public
8864
+ */
8865
+ declare class NoteNotFoundError extends NoteNotFoundErrorBase<{
8866
+ readonly name: string;
8867
+ readonly id: string;
8868
+ }> {
8869
+ get message(): string;
8870
+ }
8871
+ //#endregion
8872
+ //#region src/repos/schemas/manifest.d.ts
8873
+ /**
8874
+ * A vendored-repo manifest key: non-empty, contains no `/` or `\`, and is
8875
+ * never `.` or `..` — safe to join onto a filesystem path segment
8876
+ * (`.repos/<name>`) without escaping the `.repos/` directory.
8877
+ * @public
8878
+ */
8879
+ declare const RepoName: Schema.refine<string, typeof Schema.String>;
8880
+ /** @public */
8881
+ type RepoName = typeof RepoName.Type;
8882
+ /**
8883
+ * An agent-appended note: a discovered answer stamped with the pin it was written against.
8884
+ * @public
8885
+ */
8886
+ declare const RepoNote: Schema.Struct<{
8887
+ id: typeof Schema.String;
8888
+ date: typeof Schema.String;
8889
+ ref: typeof Schema.String;
8890
+ note: Schema.filter<typeof Schema.String>;
8891
+ }>;
8892
+ /** @public */
8893
+ type RepoNote = typeof RepoNote.Type;
8894
+ /**
8895
+ * Curated per-repo orientation: layout, entry points, where to start.
8896
+ * @public
8897
+ */
8898
+ declare const RepoOrientation: Schema.Struct<{
8899
+ layout: Schema.optional<typeof Schema.String>;
8900
+ keyPaths: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.String>>;
8901
+ startHere: Schema.optional<typeof Schema.String>;
8902
+ }>;
8903
+ /** @public */
8904
+ type RepoOrientation = typeof RepoOrientation.Type;
8905
+ /**
8906
+ * One vendored repo: mechanical intent (url/ref/sparse) plus the agent brief.
8907
+ * @public
8908
+ */
8909
+ declare const RepoEntry: Schema.Struct<{
8910
+ url: Schema.filter<typeof Schema.String>;
8911
+ ref: Schema.filter<typeof Schema.String>;
8912
+ purpose: Schema.filter<typeof Schema.String>;
8913
+ sparse: Schema.optional<Schema.Array$<typeof Schema.String>>;
8914
+ orientation: Schema.optional<Schema.Struct<{
8915
+ layout: Schema.optional<typeof Schema.String>;
8916
+ keyPaths: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.String>>;
8917
+ startHere: Schema.optional<typeof Schema.String>;
8918
+ }>>;
8919
+ notes: Schema.optional<Schema.Array$<Schema.Struct<{
8920
+ id: typeof Schema.String;
8921
+ date: typeof Schema.String;
8922
+ ref: typeof Schema.String;
8923
+ note: Schema.filter<typeof Schema.String>;
8924
+ }>>>;
8925
+ }>;
8926
+ /** @public */
8927
+ type RepoEntry = typeof RepoEntry.Type;
8928
+ /**
8929
+ * The committed .repos/config.json manifest.
8930
+ * @public
8931
+ */
8932
+ declare const ReposManifestFile: Schema.Struct<{
8933
+ repos: Schema.filter<Schema.Record$<typeof Schema.String, Schema.Struct<{
8934
+ url: Schema.filter<typeof Schema.String>;
8935
+ ref: Schema.filter<typeof Schema.String>;
8936
+ purpose: Schema.filter<typeof Schema.String>;
8937
+ sparse: Schema.optional<Schema.Array$<typeof Schema.String>>;
8938
+ orientation: Schema.optional<Schema.Struct<{
8939
+ layout: Schema.optional<typeof Schema.String>;
8940
+ keyPaths: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.String>>;
8941
+ startHere: Schema.optional<typeof Schema.String>;
8942
+ }>>;
8943
+ notes: Schema.optional<Schema.Array$<Schema.Struct<{
8944
+ id: typeof Schema.String;
8945
+ date: typeof Schema.String;
8946
+ ref: typeof Schema.String;
8947
+ note: Schema.filter<typeof Schema.String>;
8948
+ }>>>;
8949
+ }>>>;
8950
+ }>;
8951
+ /** @public */
8952
+ type ReposManifestFile = typeof ReposManifestFile.Type;
8953
+ //#endregion
8954
+ //#region src/repos/schemas/reports.d.ts
8955
+ /**
8956
+ * Status of one vendored repo: gitlink presence and dirtiness, plus notes
8957
+ * that no longer match the pinned ref.
8958
+ * @public
8959
+ */
8960
+ declare const RepoStatusEntry: Schema.Struct<{
8961
+ name: typeof Schema.String;
8962
+ ref: typeof Schema.String;
8963
+ purpose: typeof Schema.String;
8964
+ present: typeof Schema.Boolean;
8965
+ commit: Schema.NullOr<typeof Schema.String>;
8966
+ dirty: typeof Schema.Boolean;
8967
+ staleNoteIds: Schema.Array$<typeof Schema.String>;
8968
+ }>;
8969
+ /** @public */
8970
+ type RepoStatusEntry = typeof RepoStatusEntry.Type;
8971
+ /**
8972
+ * Status across all vendored repos in the manifest.
8973
+ * @public
8974
+ */
8975
+ declare const ReposStatusReport: Schema.Struct<{
8976
+ repos: Schema.Array$<Schema.Struct<{
8977
+ name: typeof Schema.String;
8978
+ ref: typeof Schema.String;
8979
+ purpose: typeof Schema.String;
8980
+ present: typeof Schema.Boolean;
8981
+ commit: Schema.NullOr<typeof Schema.String>;
8982
+ dirty: typeof Schema.Boolean;
8983
+ staleNoteIds: Schema.Array$<typeof Schema.String>;
8984
+ }>>;
8985
+ clean: typeof Schema.Boolean;
8986
+ }>;
8987
+ /** @public */
8988
+ type ReposStatusReport = typeof ReposStatusReport.Type;
8989
+ /**
8990
+ * Result of reconciling working-tree submodules with the manifest: missing
8991
+ * repos initialized, sparse-checkout patterns re-applied, already-present
8992
+ * repos left alone, and stale locks cleared.
8993
+ * @public
8994
+ */
8995
+ declare const ReposSyncReport: Schema.Struct<{
8996
+ initialized: Schema.Array$<typeof Schema.String>;
8997
+ sparseApplied: Schema.Array$<typeof Schema.String>;
8998
+ upToDate: Schema.Array$<typeof Schema.String>;
8999
+ clearedLocks: Schema.Array$<typeof Schema.String>;
9000
+ }>;
9001
+ /** @public */
9002
+ type ReposSyncReport = typeof ReposSyncReport.Type;
9003
+ /**
9004
+ * Result of re-pinning a vendored repo to a new ref.
9005
+ * @public
9006
+ */
9007
+ declare const ReposPinResult: Schema.Struct<{
9008
+ name: typeof Schema.String;
9009
+ ref: typeof Schema.String;
9010
+ oldCommit: Schema.NullOr<typeof Schema.String>;
9011
+ newCommit: typeof Schema.String;
9012
+ commitMessage: typeof Schema.String;
9013
+ staleNoteIds: Schema.Array$<typeof Schema.String>;
9014
+ }>;
9015
+ /** @public */
9016
+ type ReposPinResult = typeof ReposPinResult.Type;
9017
+ /**
9018
+ * Result of adding a new vendored repo to the manifest.
9019
+ * @public
9020
+ */
9021
+ declare const ReposAddResult: Schema.Struct<{
9022
+ name: typeof Schema.String;
9023
+ ref: typeof Schema.String;
9024
+ path: typeof Schema.String;
9025
+ }>;
9026
+ /** @public */
9027
+ type ReposAddResult = typeof ReposAddResult.Type;
9028
+ /**
9029
+ * Result of an agent-note mutation against a vendored repo.
9030
+ * @public
9031
+ */
9032
+ declare const ReposNoteResult: Schema.Struct<{
9033
+ name: typeof Schema.String;
9034
+ op: Schema.Literal<["add", "remove", "promote"]>;
9035
+ id: typeof Schema.String;
9036
+ noteCount: typeof Schema.Number;
9037
+ }>;
9038
+ /** @public */
9039
+ type ReposNoteResult = typeof ReposNoteResult.Type;
9040
+ //#endregion
9041
+ //#region src/repos/services/config-store.d.ts
9042
+ /** @internal */
9043
+ interface ReposConfigStoreShape {
9044
+ readonly exists: (root: string) => Effect.Effect<boolean>;
9045
+ readonly read: (root: string) => Effect.Effect<ReposManifestFile, ReposConfigError>;
9046
+ readonly write: (root: string, manifest: ReposManifestFile) => Effect.Effect<void, ReposConfigError>;
9047
+ }
9048
+ /** @internal */
9049
+ declare const ReposConfigStoreBase: Context.TagClass<ReposConfigStore, "@savvy-web/silk-effects/ReposConfigStore", ReposConfigStoreShape>;
9050
+ /**
9051
+ * Reads, validates, and writes the .repos/config.json manifest.
9052
+ * @public
9053
+ */
9054
+ declare class ReposConfigStore extends ReposConfigStoreBase {}
9055
+ /**
9056
+ * Live layer over the platform FileSystem.
9057
+ * @public
9058
+ */
9059
+ declare const ReposConfigStoreLive: Layer.Layer<ReposConfigStore, never, FileSystem.FileSystem | Path.Path>;
9060
+ //#endregion
9061
+ //#region src/repos/services/manager.d.ts
9062
+ /**
9063
+ * Minimum age (in milliseconds) a `.lock` file must reach before `sync` will
9064
+ * remove it. An ACTIVE git process can legitimately hold
9065
+ * `index.lock`/`shallow.lock` for the duration of its own run; removing a
9066
+ * young lock out from under it would corrupt the submodule. Ten minutes
9067
+ * comfortably exceeds any single shallow fetch/checkout this manager
9068
+ * performs, while still reclaiming locks abandoned by a process that was
9069
+ * killed or crashed. A lock younger than this is left in place — the
9070
+ * subsequent git operation fails naturally if it is genuinely contested,
9071
+ * and that failure already propagates.
9072
+ * @public
9073
+ */
9074
+ declare const STALE_LOCK_MAX_AGE_MS: number;
9075
+ /**
9076
+ * The full contractual surface of {@link ReposManager}. All five methods are
9077
+ * implemented against real git plumbing and the manifest store: `status`
9078
+ * reports drift, `sync` reconciles the working tree with the manifest,
9079
+ * `add` vendors a new repo, `pin` re-pins an existing entry to a new ref,
9080
+ * and `note` adds, removes, or promotes an agent note.
9081
+ * @internal
9082
+ */
9083
+ interface ReposManagerShape {
9084
+ readonly status: (root: string) => Effect.Effect<ReposStatusReport, ReposConfigError | GitSubmoduleError>;
9085
+ readonly sync: (root: string) => Effect.Effect<ReposSyncReport, ReposConfigError | GitSubmoduleError>;
9086
+ readonly add: (root: string, options: {
9087
+ readonly url: string;
9088
+ readonly ref: string;
9089
+ readonly purpose: string;
9090
+ readonly name?: string;
9091
+ readonly sparse?: ReadonlyArray<string>;
9092
+ }) => Effect.Effect<ReposAddResult, ReposConfigError | GitSubmoduleError>;
9093
+ readonly pin: (root: string, name: string, ref: string) => Effect.Effect<ReposPinResult, ReposConfigError | GitSubmoduleError | RepoNotFoundError>;
9094
+ readonly note: (root: string, name: string, op: {
9095
+ readonly op: "add";
9096
+ readonly note: string;
9097
+ } | {
9098
+ readonly op: "remove";
9099
+ readonly id: string;
9100
+ } | {
9101
+ readonly op: "promote";
9102
+ readonly id: string;
9103
+ readonly into: "layout" | "startHere";
9104
+ }) => Effect.Effect<ReposNoteResult, ReposConfigError | RepoNotFoundError | NoteNotFoundError>;
9105
+ }
9106
+ /** @internal */
9107
+ declare const ReposManagerBase: Context.TagClass<ReposManager, "@savvy-web/silk-effects/ReposManager", ReposManagerShape>;
9108
+ /**
9109
+ * Drives the vendored `.repos/` submodules over git: reports status
9110
+ * (presence, dirtiness, stale notes), reconciles the working tree with the
9111
+ * manifest, vendors new entries (`add`), re-pins existing entries to a new
9112
+ * ref (`pin`), and adds, removes, or promotes agent notes (`note`).
9113
+ * @public
9114
+ */
9115
+ declare class ReposManager extends ReposManagerBase {}
9116
+ /**
9117
+ * Live implementation of {@link ReposManager}.
9118
+ *
9119
+ * @remarks
9120
+ * Mirrors `TurboInspector`: the `CommandExecutor` is captured once at layer
9121
+ * construction and discharged onto each git invocation via
9122
+ * `Effect.provideService`, so the public method effects stay at `R = never`.
9123
+ * @public
9124
+ */
9125
+ declare const ReposManagerLive: Layer.Layer<ReposManager, never, ReposConfigStore | CommandExecutor.CommandExecutor | FileSystem.FileSystem | Path.Path>;
9126
+ declare namespace index_d_exports$3 {
9127
+ export { GitSubmoduleError, GitSubmoduleErrorBase, MANIFEST_PATH, NOTE_LIMIT, NoteNotFoundError, NoteNotFoundErrorBase, REPOS_DIR, RepoEntry, RepoName, RepoNotFoundError, RepoNotFoundErrorBase, RepoNote, RepoOrientation, RepoStatusEntry, ReposAddResult, ReposConfigError, ReposConfigErrorBase, ReposConfigStore, ReposConfigStoreBase, ReposConfigStoreLive, ReposConfigStoreShape, ReposManager, ReposManagerBase, ReposManagerLive, ReposManagerShape, ReposManifestFile, ReposNoteResult, ReposPinResult, ReposStatusReport, ReposSyncReport, STALE_LOCK_MAX_AGE_MS };
9128
+ }
9129
+ //#endregion
8790
9130
  //#region src/schemas/BiomeConfig.d.ts
8791
9131
  /**
8792
9132
  * Result of a Biome schema URL sync or check operation.
@@ -10556,9 +10896,9 @@ declare class TurboInspector extends TurboInspector_base {}
10556
10896
  * @since 0.7.0
10557
10897
  */
10558
10898
  declare const TurboInspectorLive: Layer.Layer<TurboInspector, never, ToolDiscovery | CommandExecutor.CommandExecutor | FileSystem.FileSystem>;
10559
- declare namespace index_d_exports$3 {
10899
+ declare namespace index_d_exports$4 {
10560
10900
  export { AffectedResult, AffectedResultType, CacheDiagnosis, CacheDiagnosisType, DryRunParseError, GlobalHashSummary, GraphNode, MissExplanation, NotATurboRepoError, PackageCacheStatus, TaskGraphResult, TaskGraphResultType, TurboCache, TurboDigest, TurboDryRun, TurboDryRunType, TurboDryTask, TurboDryTaskType, TurboEnvVars, TurboError, TurboExecError, TurboGlobalCacheInputs, TurboInspector, TurboInspectorLive, TurboNotInstalledError };
10561
10901
  }
10562
10902
  //#endregion
10563
- export { AnalyzedWorkspace, BiomeSchemaSync, BiomeSchemaSyncLive, BiomeSyncError, type BiomeSyncOptions, type BiomeSyncResult, ChangesetConfig, ChangesetConfigError, type ChangesetConfigFile, ChangesetConfigLive, ChangesetConfigReader, ChangesetConfigReaderLive, type ChangesetMode, index_d_exports as Changesets, CheckResult, type CheckResultDefinition, type CommentStyle, index_d_exports$1 as Commitlint, type CommitlintPlugin, type CommitlintUserConfig, ConfigDiscovery, ConfigDiscoveryLive, type ConfigDiscoveryOptions, type ConfigLocation, ConfigNotFoundError, type ConfigSource, index_d_exports$2 as Lint, ManagedSection, ManagedSectionLive, type PromptConfig, type PromptSettings, PublishTargetBindingError, PublishabilityDetectorAdaptiveLive, type PublishablePackage, type RawPackageJson, type RawPublishConfig, type RawPublishTargets, type RawTargetObject, type RawTargetValue, ResolutionPolicy, type ResolutionPolicyDefinition, ResolvedTool, type RuleApplicability, type RuleConfigTuple, type RuleSeverity, type RulesConfig, SavvyBaseSection, SavvyHooksSection, SectionBlock, SectionDefinition, SectionDiff, type SectionDiffDefinition, SectionParseError, SectionValidationError, SectionWriteError, ShellSectionDefinition, type SilkChangesetConfigFile, SilkPublishConfig, SilkPublishability, SilkPublishabilityDetectorLive, SilkWorkspaceAnalyzer, SilkWorkspaceAnalyzerLive, SourceRequirement, type SourceRequirementDefinition, SyncResult, type SyncResultDefinition, TagFormatError, TagStrategy, TagStrategyLive, type TagStrategyType, type TargetBinding, type TargetGroupBinding, type TargetsBinding, ToolCommand, ToolDefinition, ToolDiscovery, ToolDiscoveryLive, ToolNotFoundError, ToolResolutionError, ToolSource, ToolVersionMismatchError, index_d_exports$3 as Turbo, VersionExtractor, type VersionExtractorDefinition, VersioningDetectionError, VersioningStrategy, VersioningStrategyLive, type VersioningStrategyResult, type VersioningStrategyType, WorkspaceAnalysis, WorkspaceAnalysisError, buildSchemaUrl, extractSemver, readTargetsBinding, savvyBasePreamble, savvyHooksHygiene, savvyToolSection };
10903
+ export { AnalyzedWorkspace, BiomeSchemaSync, BiomeSchemaSyncLive, BiomeSyncError, type BiomeSyncOptions, type BiomeSyncResult, ChangesetConfig, ChangesetConfigError, type ChangesetConfigFile, ChangesetConfigLive, ChangesetConfigReader, ChangesetConfigReaderLive, type ChangesetMode, index_d_exports as Changesets, CheckResult, type CheckResultDefinition, type CommentStyle, index_d_exports$1 as Commitlint, type CommitlintPlugin, type CommitlintUserConfig, ConfigDiscovery, ConfigDiscoveryLive, type ConfigDiscoveryOptions, type ConfigLocation, ConfigNotFoundError, type ConfigSource, index_d_exports$2 as Lint, ManagedSection, ManagedSectionLive, type PromptConfig, type PromptSettings, PublishTargetBindingError, PublishabilityDetectorAdaptiveLive, type PublishablePackage, type RawPackageJson, type RawPublishConfig, type RawPublishTargets, type RawTargetObject, type RawTargetValue, index_d_exports$3 as Repos, ResolutionPolicy, type ResolutionPolicyDefinition, ResolvedTool, type RuleApplicability, type RuleConfigTuple, type RuleSeverity, type RulesConfig, SavvyBaseSection, SavvyHooksSection, SectionBlock, SectionDefinition, SectionDiff, type SectionDiffDefinition, SectionParseError, SectionValidationError, SectionWriteError, ShellSectionDefinition, type SilkChangesetConfigFile, SilkPublishConfig, SilkPublishability, SilkPublishabilityDetectorLive, SilkWorkspaceAnalyzer, SilkWorkspaceAnalyzerLive, SourceRequirement, type SourceRequirementDefinition, SyncResult, type SyncResultDefinition, TagFormatError, TagStrategy, TagStrategyLive, type TagStrategyType, type TargetBinding, type TargetGroupBinding, type TargetsBinding, ToolCommand, ToolDefinition, ToolDiscovery, ToolDiscoveryLive, ToolNotFoundError, ToolResolutionError, ToolSource, ToolVersionMismatchError, index_d_exports$4 as Turbo, VersionExtractor, type VersionExtractorDefinition, VersioningDetectionError, VersioningStrategy, VersioningStrategyLive, type VersioningStrategyResult, type VersioningStrategyType, WorkspaceAnalysis, WorkspaceAnalysisError, buildSchemaUrl, extractSemver, readTargetsBinding, savvyBasePreamble, savvyHooksHygiene, savvyToolSection };
10564
10904
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -21,6 +21,7 @@ import { SectionBlock } from "./schemas/SectionBlock.js";
21
21
  import { SectionDefinition, ShellSectionDefinition } from "./schemas/SectionDefinition.js";
22
22
  import { SavvyBaseSection, SavvyHooksSection, savvyBasePreamble, savvyHooksHygiene, savvyToolSection } from "./schemas/SavvySections.js";
23
23
  import { lint_exports } from "./lint/index.js";
24
+ import { repos_exports } from "./repos/index.js";
24
25
  import { ToolCommand } from "./utils/ToolCommand.js";
25
26
  import { ResolutionPolicy, SourceRequirement, ToolSource, VersionExtractor } from "./schemas/ToolResults.js";
26
27
  import { ResolvedTool } from "./schemas/ResolvedTool.js";
@@ -35,4 +36,4 @@ import { SilkWorkspaceAnalyzer, SilkWorkspaceAnalyzerLive } from "./services/Sil
35
36
  import { ToolDiscovery, ToolDiscoveryLive } from "./services/ToolDiscovery.js";
36
37
  import { turbo_exports } from "./turbo/index.js";
37
38
 
38
- export { AnalyzedWorkspace, BiomeSchemaSync, BiomeSchemaSyncLive, BiomeSyncError, ChangesetConfig, ChangesetConfigError, ChangesetConfigLive, ChangesetConfigReader, ChangesetConfigReaderLive, changesets_exports as Changesets, CheckResult, commitlint_exports as Commitlint, ConfigDiscovery, ConfigDiscoveryLive, ConfigNotFoundError, lint_exports as Lint, ManagedSection, ManagedSectionLive, PublishTargetBindingError, PublishabilityDetectorAdaptiveLive, ResolutionPolicy, ResolvedTool, SavvyBaseSection, SavvyHooksSection, SectionBlock, SectionDefinition, SectionDiff, SectionParseError, SectionValidationError, SectionWriteError, ShellSectionDefinition, SilkPublishConfig, SilkPublishability, SilkPublishabilityDetectorLive, SilkWorkspaceAnalyzer, SilkWorkspaceAnalyzerLive, SourceRequirement, SyncResult, TagFormatError, TagStrategy, TagStrategyLive, ToolCommand, ToolDefinition, ToolDiscovery, ToolDiscoveryLive, ToolNotFoundError, ToolResolutionError, ToolSource, ToolVersionMismatchError, turbo_exports as Turbo, VersionExtractor, VersioningDetectionError, VersioningStrategy, VersioningStrategyLive, WorkspaceAnalysis, WorkspaceAnalysisError, buildSchemaUrl, extractSemver, readTargetsBinding, savvyBasePreamble, savvyHooksHygiene, savvyToolSection };
39
+ export { AnalyzedWorkspace, BiomeSchemaSync, BiomeSchemaSyncLive, BiomeSyncError, ChangesetConfig, ChangesetConfigError, ChangesetConfigLive, ChangesetConfigReader, ChangesetConfigReaderLive, changesets_exports as Changesets, CheckResult, commitlint_exports as Commitlint, ConfigDiscovery, ConfigDiscoveryLive, ConfigNotFoundError, lint_exports as Lint, ManagedSection, ManagedSectionLive, PublishTargetBindingError, PublishabilityDetectorAdaptiveLive, repos_exports as Repos, ResolutionPolicy, ResolvedTool, SavvyBaseSection, SavvyHooksSection, SectionBlock, SectionDefinition, SectionDiff, SectionParseError, SectionValidationError, SectionWriteError, ShellSectionDefinition, SilkPublishConfig, SilkPublishability, SilkPublishabilityDetectorLive, SilkWorkspaceAnalyzer, SilkWorkspaceAnalyzerLive, SourceRequirement, SyncResult, TagFormatError, TagStrategy, TagStrategyLive, ToolCommand, ToolDefinition, ToolDiscovery, ToolDiscoveryLive, ToolNotFoundError, ToolResolutionError, ToolSource, ToolVersionMismatchError, turbo_exports as Turbo, VersionExtractor, VersioningDetectionError, VersioningStrategy, VersioningStrategyLive, WorkspaceAnalysis, WorkspaceAnalysisError, buildSchemaUrl, extractSemver, readTargetsBinding, savvyBasePreamble, savvyHooksHygiene, savvyToolSection };
@@ -26,7 +26,8 @@ const MARKDOWNLINT_TEMPLATE = {
26
26
  "**/.claude/plans",
27
27
  "**/docs/superpowers",
28
28
  "**/__test__/**/fixtures/**",
29
- "**/__fixtures__/**"
29
+ "**/__fixtures__/**",
30
+ "**/.repos"
30
31
  ],
31
32
  customRules: ["@savvy-web/silk/changesets/markdownlint"],
32
33
  config: {
@@ -4,11 +4,16 @@ import { Filter } from "../utils/Filter.js";
4
4
  /**
5
5
  * Handler for shell script files.
6
6
  *
7
- * Removes executable bit by default (security best practice).
7
+ * Removes the executable bit by default: Silk repos invoke shell scripts via
8
+ * `bash <script>`, so the bit is never needed at runtime, and normalizing the
9
+ * mode keeps diffs clean. Committed `.sh` files landing as `100644` is
10
+ * intentional, not mode drift.
8
11
  *
9
12
  * @remarks
10
- * By default, excludes `.claude/scripts/` which need to remain executable
11
- * for lint-staged hooks to work.
13
+ * The default excludes `.claude/scripts/` as a consumer escape-hatch
14
+ * convention: a repo that needs a script to stay executable across commits
15
+ * can place it there (or pass its own `exclude` list). Nothing in Silk itself
16
+ * requires the directory to exist.
12
17
  *
13
18
  * @example
14
19
  * ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/silk-effects",
3
- "version": "3.2.5",
3
+ "version": "3.3.1",
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",
@@ -47,7 +47,7 @@
47
47
  "unified": "^11.0.5",
48
48
  "unified-lint-rule": "^3.0.1",
49
49
  "unist-util-visit": "^5.1.0",
50
- "workspaces-effect": "^2.0.3",
50
+ "workspaces-effect": "^2.1.0",
51
51
  "yaml": "^2.9.0",
52
52
  "yaml-effect": "^0.7.2",
53
53
  "yaml-lint": "^1.7.0"
@@ -0,0 +1,20 @@
1
+ //#region src/repos/constants.ts
2
+ /**
3
+ * Directory vendored reference repos live under, relative to the repo root.
4
+ * @public
5
+ */
6
+ const REPOS_DIR = ".repos";
7
+ /**
8
+ * Path of the committed vendored-repos manifest, relative to the repo root.
9
+ * @public
10
+ */
11
+ const MANIFEST_PATH = ".repos/config.json";
12
+ /**
13
+ * Maximum notes per vendored repo; enforced at write time to force
14
+ * consolidation into orientation.
15
+ * @public
16
+ */
17
+ const NOTE_LIMIT = 10;
18
+
19
+ //#endregion
20
+ export { MANIFEST_PATH, REPOS_DIR };
@@ -0,0 +1,50 @@
1
+ import { Data } from "effect";
2
+
3
+ //#region src/repos/errors.ts
4
+ /** @internal */
5
+ const ReposConfigErrorBase = Data.TaggedError("ReposConfigError");
6
+ /**
7
+ * The .repos/config.json manifest is missing, unreadable, or invalid.
8
+ * @public
9
+ */
10
+ var ReposConfigError = class extends ReposConfigErrorBase {
11
+ get message() {
12
+ return `repos manifest ${this.kind} at ${this.path}: ${this.reason}`;
13
+ }
14
+ };
15
+ /** @internal */
16
+ const GitSubmoduleErrorBase = Data.TaggedError("GitSubmoduleError");
17
+ /**
18
+ * A git submodule operation failed.
19
+ * @public
20
+ */
21
+ var GitSubmoduleError = class extends GitSubmoduleErrorBase {
22
+ get message() {
23
+ return `git command failed in ${this.cwd}: ${this.command}\n${this.reason}`;
24
+ }
25
+ };
26
+ /** @internal */
27
+ const RepoNotFoundErrorBase = Data.TaggedError("RepoNotFoundError");
28
+ /**
29
+ * The named repo is not present in the manifest.
30
+ * @public
31
+ */
32
+ var RepoNotFoundError = class extends RepoNotFoundErrorBase {
33
+ get message() {
34
+ return `no vendored repo named "${this.name}" in the manifest`;
35
+ }
36
+ };
37
+ /** @internal */
38
+ const NoteNotFoundErrorBase = Data.TaggedError("NoteNotFoundError");
39
+ /**
40
+ * The note id does not exist on the named repo.
41
+ * @public
42
+ */
43
+ var NoteNotFoundError = class extends NoteNotFoundErrorBase {
44
+ get message() {
45
+ return `no note "${this.id}" on vendored repo "${this.name}"`;
46
+ }
47
+ };
48
+
49
+ //#endregion
50
+ export { GitSubmoduleError, GitSubmoduleErrorBase, NoteNotFoundError, NoteNotFoundErrorBase, RepoNotFoundError, RepoNotFoundErrorBase, ReposConfigError, ReposConfigErrorBase };
package/repos/index.js ADDED
@@ -0,0 +1,43 @@
1
+ import { __exportAll } from "../_virtual/_rolldown/runtime.js";
2
+ import { MANIFEST_PATH, REPOS_DIR } from "./constants.js";
3
+ import { GitSubmoduleError, GitSubmoduleErrorBase, NoteNotFoundError, NoteNotFoundErrorBase, RepoNotFoundError, RepoNotFoundErrorBase, ReposConfigError, ReposConfigErrorBase } from "./errors.js";
4
+ import { RepoEntry, RepoName, RepoNote, RepoOrientation, ReposManifestFile } from "./schemas/manifest.js";
5
+ import { RepoStatusEntry, ReposAddResult, ReposNoteResult, ReposPinResult, ReposStatusReport, ReposSyncReport } from "./schemas/reports.js";
6
+ import { ReposConfigStore, ReposConfigStoreBase, ReposConfigStoreLive } from "./services/config-store.js";
7
+ import { ReposManager, ReposManagerBase, ReposManagerLive, STALE_LOCK_MAX_AGE_MS } from "./services/manager.js";
8
+
9
+ //#region src/repos/index.ts
10
+ var repos_exports = /* @__PURE__ */ __exportAll({
11
+ GitSubmoduleError: () => GitSubmoduleError,
12
+ GitSubmoduleErrorBase: () => GitSubmoduleErrorBase,
13
+ MANIFEST_PATH: () => MANIFEST_PATH,
14
+ NOTE_LIMIT: () => 10,
15
+ NoteNotFoundError: () => NoteNotFoundError,
16
+ NoteNotFoundErrorBase: () => NoteNotFoundErrorBase,
17
+ REPOS_DIR: () => REPOS_DIR,
18
+ RepoEntry: () => RepoEntry,
19
+ RepoName: () => RepoName,
20
+ RepoNotFoundError: () => RepoNotFoundError,
21
+ RepoNotFoundErrorBase: () => RepoNotFoundErrorBase,
22
+ RepoNote: () => RepoNote,
23
+ RepoOrientation: () => RepoOrientation,
24
+ RepoStatusEntry: () => RepoStatusEntry,
25
+ ReposAddResult: () => ReposAddResult,
26
+ ReposConfigError: () => ReposConfigError,
27
+ ReposConfigErrorBase: () => ReposConfigErrorBase,
28
+ ReposConfigStore: () => ReposConfigStore,
29
+ ReposConfigStoreBase: () => ReposConfigStoreBase,
30
+ ReposConfigStoreLive: () => ReposConfigStoreLive,
31
+ ReposManager: () => ReposManager,
32
+ ReposManagerBase: () => ReposManagerBase,
33
+ ReposManagerLive: () => ReposManagerLive,
34
+ ReposManifestFile: () => ReposManifestFile,
35
+ ReposNoteResult: () => ReposNoteResult,
36
+ ReposPinResult: () => ReposPinResult,
37
+ ReposStatusReport: () => ReposStatusReport,
38
+ ReposSyncReport: () => ReposSyncReport,
39
+ STALE_LOCK_MAX_AGE_MS: () => STALE_LOCK_MAX_AGE_MS
40
+ });
41
+
42
+ //#endregion
43
+ export { GitSubmoduleError, GitSubmoduleErrorBase, MANIFEST_PATH, NoteNotFoundError, NoteNotFoundErrorBase, REPOS_DIR, RepoEntry, RepoName, RepoNotFoundError, RepoNotFoundErrorBase, RepoNote, RepoOrientation, RepoStatusEntry, ReposAddResult, ReposConfigError, ReposConfigErrorBase, ReposConfigStore, ReposConfigStoreBase, ReposConfigStoreLive, ReposManager, ReposManagerBase, ReposManagerLive, ReposManifestFile, ReposNoteResult, ReposPinResult, ReposStatusReport, ReposSyncReport, STALE_LOCK_MAX_AGE_MS, repos_exports };
@@ -0,0 +1,73 @@
1
+ import { Schema } from "effect";
2
+
3
+ //#region src/repos/schemas/manifest.ts
4
+ /**
5
+ * A vendored-repo manifest key: non-empty, contains no `/` or `\`, and is
6
+ * never `.` or `..` — safe to join onto a filesystem path segment
7
+ * (`.repos/<name>`) without escaping the `.repos/` directory.
8
+ * @public
9
+ */
10
+ const RepoName = Schema.String.pipe(Schema.pattern(/^(?!\.{1,2}$)[A-Za-z0-9][A-Za-z0-9._-]*$/), Schema.annotations({ identifier: "RepoName" }));
11
+ /**
12
+ * An agent-appended note: a discovered answer stamped with the pin it was written against.
13
+ * @public
14
+ */
15
+ const RepoNote = Schema.Struct({
16
+ id: Schema.String,
17
+ date: Schema.String,
18
+ ref: Schema.String,
19
+ note: Schema.String.pipe(Schema.minLength(1))
20
+ });
21
+ /**
22
+ * Curated per-repo orientation: layout, entry points, where to start.
23
+ * @public
24
+ */
25
+ const RepoOrientation = Schema.Struct({
26
+ layout: Schema.optional(Schema.String),
27
+ keyPaths: Schema.optional(Schema.Record({
28
+ key: Schema.String,
29
+ value: Schema.String
30
+ })),
31
+ startHere: Schema.optional(Schema.String)
32
+ });
33
+ /**
34
+ * One vendored repo: mechanical intent (url/ref/sparse) plus the agent brief.
35
+ * @public
36
+ */
37
+ const RepoEntry = Schema.Struct({
38
+ url: Schema.String.pipe(Schema.minLength(1)),
39
+ ref: Schema.String.pipe(Schema.minLength(1)),
40
+ purpose: Schema.String.pipe(Schema.minLength(1)),
41
+ sparse: Schema.optional(Schema.Array(Schema.String)),
42
+ orientation: Schema.optional(RepoOrientation),
43
+ notes: Schema.optional(Schema.Array(RepoNote))
44
+ });
45
+ /**
46
+ * Bad-key guard for {@link ReposManifestFile}.
47
+ *
48
+ * @remarks
49
+ * `Schema.Record`'s key schema has "pick matching keys" semantics: a key
50
+ * that fails to decode against the key schema is silently OMITTED from the
51
+ * decoded record rather than failing the decode (Effect Schema issue-free
52
+ * by design, since a record's key schema is also used to select entries out
53
+ * of a wider object). That is the opposite of what a manifest guard needs —
54
+ * a bad key must REJECT the whole decode, not vanish. So `repos` decodes
55
+ * with a permissive `Schema.String` key (nothing is dropped) and this
56
+ * filter walks every key against {@link RepoName} itself, failing loudly
57
+ * and naming the offending key.
58
+ */
59
+ const isRepoName = Schema.is(RepoName);
60
+ /**
61
+ * The committed .repos/config.json manifest.
62
+ * @public
63
+ */
64
+ const ReposManifestFile = Schema.Struct({ repos: Schema.Record({
65
+ key: Schema.String,
66
+ value: RepoEntry
67
+ }).pipe(Schema.filter((repos) => {
68
+ const badKey = Object.keys(repos).find((key) => !isRepoName(key));
69
+ return badKey === void 0 ? void 0 : `every repos key must be a valid RepoName (non-empty, no "/" or "\\", not "." or ".."); got ${JSON.stringify(badKey)}`;
70
+ })) });
71
+
72
+ //#endregion
73
+ export { RepoEntry, RepoName, RepoNote, RepoOrientation, ReposManifestFile };
@@ -0,0 +1,71 @@
1
+ import { Schema } from "effect";
2
+
3
+ //#region src/repos/schemas/reports.ts
4
+ /**
5
+ * Status of one vendored repo: gitlink presence and dirtiness, plus notes
6
+ * that no longer match the pinned ref.
7
+ * @public
8
+ */
9
+ const RepoStatusEntry = Schema.Struct({
10
+ name: Schema.String,
11
+ ref: Schema.String,
12
+ purpose: Schema.String,
13
+ present: Schema.Boolean,
14
+ commit: Schema.NullOr(Schema.String),
15
+ dirty: Schema.Boolean,
16
+ staleNoteIds: Schema.Array(Schema.String)
17
+ });
18
+ /**
19
+ * Status across all vendored repos in the manifest.
20
+ * @public
21
+ */
22
+ const ReposStatusReport = Schema.Struct({
23
+ repos: Schema.Array(RepoStatusEntry),
24
+ clean: Schema.Boolean
25
+ });
26
+ /**
27
+ * Result of reconciling working-tree submodules with the manifest: missing
28
+ * repos initialized, sparse-checkout patterns re-applied, already-present
29
+ * repos left alone, and stale locks cleared.
30
+ * @public
31
+ */
32
+ const ReposSyncReport = Schema.Struct({
33
+ initialized: Schema.Array(Schema.String),
34
+ sparseApplied: Schema.Array(Schema.String),
35
+ upToDate: Schema.Array(Schema.String),
36
+ clearedLocks: Schema.Array(Schema.String)
37
+ });
38
+ /**
39
+ * Result of re-pinning a vendored repo to a new ref.
40
+ * @public
41
+ */
42
+ const ReposPinResult = Schema.Struct({
43
+ name: Schema.String,
44
+ ref: Schema.String,
45
+ oldCommit: Schema.NullOr(Schema.String),
46
+ newCommit: Schema.String,
47
+ commitMessage: Schema.String,
48
+ staleNoteIds: Schema.Array(Schema.String)
49
+ });
50
+ /**
51
+ * Result of adding a new vendored repo to the manifest.
52
+ * @public
53
+ */
54
+ const ReposAddResult = Schema.Struct({
55
+ name: Schema.String,
56
+ ref: Schema.String,
57
+ path: Schema.String
58
+ });
59
+ /**
60
+ * Result of an agent-note mutation against a vendored repo.
61
+ * @public
62
+ */
63
+ const ReposNoteResult = Schema.Struct({
64
+ name: Schema.String,
65
+ op: Schema.Literal("add", "remove", "promote"),
66
+ id: Schema.String,
67
+ noteCount: Schema.Number
68
+ });
69
+
70
+ //#endregion
71
+ export { RepoStatusEntry, ReposAddResult, ReposNoteResult, ReposPinResult, ReposStatusReport, ReposSyncReport };
@@ -0,0 +1,86 @@
1
+ import { MANIFEST_PATH, REPOS_DIR } from "../constants.js";
2
+ import { ReposConfigError } from "../errors.js";
3
+ import { ReposManifestFile } from "../schemas/manifest.js";
4
+ import { Context, Effect, Layer, Schema } from "effect";
5
+ import { FileSystem, Path } from "@effect/platform";
6
+
7
+ //#region src/repos/services/config-store.ts
8
+ const _tag = Context.Tag("@savvy-web/silk-effects/ReposConfigStore");
9
+ /** @internal */
10
+ const ReposConfigStoreBase = _tag();
11
+ /**
12
+ * Reads, validates, and writes the .repos/config.json manifest.
13
+ * @public
14
+ */
15
+ var ReposConfigStore = class extends ReposConfigStoreBase {};
16
+ /**
17
+ * Live layer over the platform FileSystem.
18
+ * @public
19
+ */
20
+ const ReposConfigStoreLive = Layer.effect(ReposConfigStore, Effect.gen(function* () {
21
+ const fs = yield* FileSystem.FileSystem;
22
+ const path = yield* Path.Path;
23
+ const manifestPath = (root) => path.join(root, MANIFEST_PATH);
24
+ const exists = (root) => fs.exists(manifestPath(root)).pipe(Effect.orElseSucceed(() => false));
25
+ const read = (root) => Effect.gen(function* () {
26
+ if (!(yield* fs.exists(manifestPath(root)).pipe(Effect.mapError((cause) => new ReposConfigError({
27
+ path: manifestPath(root),
28
+ reason: `stat failed: ${String(cause)}`,
29
+ kind: "invalid"
30
+ }))))) return yield* Effect.fail(new ReposConfigError({
31
+ path: manifestPath(root),
32
+ reason: "no such file",
33
+ kind: "missing"
34
+ }));
35
+ const text = yield* fs.readFileString(manifestPath(root)).pipe(Effect.mapError((cause) => new ReposConfigError({
36
+ path: manifestPath(root),
37
+ reason: String(cause),
38
+ kind: "invalid"
39
+ })));
40
+ const json = yield* Effect.try({
41
+ try: () => JSON.parse(text),
42
+ catch: (cause) => new ReposConfigError({
43
+ path: manifestPath(root),
44
+ reason: `invalid JSON: ${String(cause)}`,
45
+ kind: "invalid"
46
+ })
47
+ });
48
+ return yield* Schema.decodeUnknown(ReposManifestFile)(json).pipe(Effect.mapError((cause) => new ReposConfigError({
49
+ path: manifestPath(root),
50
+ reason: String(cause),
51
+ kind: "invalid"
52
+ })));
53
+ });
54
+ const write = (root, manifest) => Effect.gen(function* () {
55
+ const dir = path.join(root, REPOS_DIR);
56
+ yield* fs.makeDirectory(dir, { recursive: true }).pipe(Effect.mapError((cause) => new ReposConfigError({
57
+ path: manifestPath(root),
58
+ reason: `mkdir failed: ${String(cause)}`,
59
+ kind: "invalid"
60
+ })));
61
+ const encoded = yield* Schema.encode(ReposManifestFile)(manifest).pipe(Effect.mapError((cause) => new ReposConfigError({
62
+ path: manifestPath(root),
63
+ reason: String(cause),
64
+ kind: "invalid"
65
+ })));
66
+ const tmpPath = `${manifestPath(root)}.tmp`;
67
+ yield* fs.writeFileString(tmpPath, `${JSON.stringify(encoded, null, " ")}\n`).pipe(Effect.mapError((cause) => new ReposConfigError({
68
+ path: manifestPath(root),
69
+ reason: String(cause),
70
+ kind: "invalid"
71
+ })));
72
+ yield* fs.rename(tmpPath, manifestPath(root)).pipe(Effect.mapError((cause) => new ReposConfigError({
73
+ path: manifestPath(root),
74
+ reason: `rename failed: ${String(cause)}`,
75
+ kind: "invalid"
76
+ })));
77
+ });
78
+ return {
79
+ exists,
80
+ read,
81
+ write
82
+ };
83
+ }));
84
+
85
+ //#endregion
86
+ export { ReposConfigStore, ReposConfigStoreBase, ReposConfigStoreLive };
@@ -0,0 +1,384 @@
1
+ import { MANIFEST_PATH, REPOS_DIR } from "../constants.js";
2
+ import { GitSubmoduleError, NoteNotFoundError, RepoNotFoundError, ReposConfigError } from "../errors.js";
3
+ import { RepoName } from "../schemas/manifest.js";
4
+ import { ReposConfigStore } from "./config-store.js";
5
+ import { Clock, Context, Effect, Layer, Option, Schema, Stream } from "effect";
6
+ import { Command, CommandExecutor, FileSystem, Path } from "@effect/platform";
7
+ import { createHash } from "node:crypto";
8
+
9
+ //#region src/repos/services/manager.ts
10
+ /**
11
+ * Lock files git leaves behind when a submodule fetch is interrupted; `sync`
12
+ * clears these before attempting to (re)initialize a submodule.
13
+ */
14
+ const STALE_LOCKS = ["index.lock", "shallow.lock"];
15
+ /**
16
+ * Minimum age (in milliseconds) a `.lock` file must reach before `sync` will
17
+ * remove it. An ACTIVE git process can legitimately hold
18
+ * `index.lock`/`shallow.lock` for the duration of its own run; removing a
19
+ * young lock out from under it would corrupt the submodule. Ten minutes
20
+ * comfortably exceeds any single shallow fetch/checkout this manager
21
+ * performs, while still reclaiming locks abandoned by a process that was
22
+ * killed or crashed. A lock younger than this is left in place — the
23
+ * subsequent git operation fails naturally if it is genuinely contested,
24
+ * and that failure already propagates.
25
+ * @public
26
+ */
27
+ const STALE_LOCK_MAX_AGE_MS = 10 * 6e4;
28
+ const _tag = Context.Tag("@savvy-web/silk-effects/ReposManager");
29
+ /** @internal */
30
+ const ReposManagerBase = _tag();
31
+ /**
32
+ * Drives the vendored `.repos/` submodules over git: reports status
33
+ * (presence, dirtiness, stale notes), reconciles the working tree with the
34
+ * manifest, vendors new entries (`add`), re-pins existing entries to a new
35
+ * ref (`pin`), and adds, removes, or promotes agent notes (`note`).
36
+ * @public
37
+ */
38
+ var ReposManager = class extends ReposManagerBase {};
39
+ /**
40
+ * Live implementation of {@link ReposManager}.
41
+ *
42
+ * @remarks
43
+ * Mirrors `TurboInspector`: the `CommandExecutor` is captured once at layer
44
+ * construction and discharged onto each git invocation via
45
+ * `Effect.provideService`, so the public method effects stay at `R = never`.
46
+ * @public
47
+ */
48
+ const ReposManagerLive = Layer.effect(ReposManager, Effect.gen(function* () {
49
+ const configStore = yield* ReposConfigStore;
50
+ const fs = yield* FileSystem.FileSystem;
51
+ const path = yield* Path.Path;
52
+ const executor = yield* CommandExecutor.CommandExecutor;
53
+ const runGit = (cwd, args) => Effect.scoped(Effect.gen(function* () {
54
+ const process = yield* executor.start(Command.workingDirectory(Command.make("git", ...args), cwd));
55
+ const [exitCode, stdout, stderr] = yield* Effect.all([
56
+ process.exitCode,
57
+ process.stdout.pipe(Stream.decodeText(), Stream.runFold("", (acc, chunk) => acc + chunk)),
58
+ process.stderr.pipe(Stream.decodeText(), Stream.runFold("", (acc, chunk) => acc + chunk))
59
+ ], { concurrency: 3 });
60
+ if (exitCode !== 0) return yield* Effect.fail(new GitSubmoduleError({
61
+ command: `git ${args.join(" ")}`,
62
+ cwd,
63
+ reason: stderr.trim().length > 0 ? stderr.trim() : `exit code ${exitCode}`
64
+ }));
65
+ return stdout.trim();
66
+ })).pipe(Effect.catchAll((cause) => Effect.fail(cause instanceof GitSubmoduleError ? cause : new GitSubmoduleError({
67
+ command: `git ${args.join(" ")}`,
68
+ cwd,
69
+ reason: String(cause)
70
+ }))));
71
+ const isPresent = (repoPath) => fs.readDirectory(repoPath).pipe(Effect.map((files) => files.length > 0), Effect.orElseSucceed(() => false));
72
+ const status = (root) => Effect.gen(function* () {
73
+ const manifest = yield* configStore.read(root);
74
+ const repos = yield* Effect.forEach(Object.entries(manifest.repos), ([name, entry]) => Effect.gen(function* () {
75
+ const repoPath = path.join(root, REPOS_DIR, name);
76
+ const present = yield* isPresent(repoPath);
77
+ const lsTree = yield* runGit(root, [
78
+ "ls-tree",
79
+ "HEAD",
80
+ "--",
81
+ `${REPOS_DIR}/${name}`
82
+ ]);
83
+ const commit = lsTree.length > 0 ? lsTree.split(/\s+/)[2] ?? null : null;
84
+ let dirty = false;
85
+ if (present) dirty = (yield* runGit(repoPath, ["status", "--porcelain"])).length > 0;
86
+ const staleNoteIds = (entry.notes ?? []).filter((note) => note.ref !== entry.ref).map((note) => note.id);
87
+ return {
88
+ name,
89
+ ref: entry.ref,
90
+ purpose: entry.purpose,
91
+ present,
92
+ commit,
93
+ dirty,
94
+ staleNoteIds
95
+ };
96
+ }));
97
+ return {
98
+ repos,
99
+ clean: repos.every((entry) => entry.present && !entry.dirty && entry.staleNoteIds.length === 0)
100
+ };
101
+ });
102
+ const sync = (root) => Effect.gen(function* () {
103
+ const manifest = yield* configStore.read(root);
104
+ const initialized = [];
105
+ const sparseApplied = [];
106
+ const upToDate = [];
107
+ const clearedLocks = [];
108
+ for (const [name, entry] of Object.entries(manifest.repos)) {
109
+ const repoPath = path.join(root, REPOS_DIR, name);
110
+ const moduleDir = path.join(root, ".git", "modules", REPOS_DIR, name);
111
+ let clearedAnyLock = false;
112
+ for (const lock of STALE_LOCKS) {
113
+ const lockPath = path.join(moduleDir, lock);
114
+ const info = yield* fs.stat(lockPath).pipe(Effect.option);
115
+ if (Option.isNone(info)) continue;
116
+ const mtime = info.value.mtime;
117
+ if (Option.isNone(mtime)) continue;
118
+ if ((yield* Clock.currentTimeMillis) - mtime.value.getTime() < 6e5) continue;
119
+ if (yield* fs.remove(lockPath).pipe(Effect.match({
120
+ onSuccess: () => true,
121
+ onFailure: () => false
122
+ }))) clearedAnyLock = true;
123
+ }
124
+ if (clearedAnyLock) clearedLocks.push(name);
125
+ if (!(yield* isPresent(repoPath))) {
126
+ yield* runGit(root, [
127
+ "submodule",
128
+ "update",
129
+ "--init",
130
+ "--depth",
131
+ "1",
132
+ "--",
133
+ `${REPOS_DIR}/${name}`
134
+ ]);
135
+ initialized.push(name);
136
+ } else upToDate.push(name);
137
+ if (entry.sparse && entry.sparse.length > 0) {
138
+ yield* runGit(repoPath, [
139
+ "sparse-checkout",
140
+ "set",
141
+ "--no-cone",
142
+ ...entry.sparse
143
+ ]);
144
+ sparseApplied.push(name);
145
+ }
146
+ }
147
+ return {
148
+ initialized,
149
+ sparseApplied,
150
+ upToDate,
151
+ clearedLocks
152
+ };
153
+ });
154
+ /** Last path segment of a repo URL, with a trailing `.git` stripped. */
155
+ const repoSlug = (url) => {
156
+ const last = url.split("/").filter((segment) => segment.length > 0).pop() ?? url;
157
+ return last.endsWith(".git") ? last.slice(0, -4) : last;
158
+ };
159
+ /**
160
+ * Fetch `ref` shallow into a submodule. Tags need the explicit
161
+ * `fetch origin tag <ref>` form; branches/commits fall back to a plain
162
+ * `fetch origin <ref>`.
163
+ */
164
+ const fetchRef = (sub, ref) => runGit(sub, [
165
+ "fetch",
166
+ "--depth",
167
+ "1",
168
+ "origin",
169
+ "tag",
170
+ ref
171
+ ]).pipe(Effect.orElse(() => runGit(sub, [
172
+ "fetch",
173
+ "--depth",
174
+ "1",
175
+ "origin",
176
+ ref
177
+ ])));
178
+ const add = (root, options) => Effect.gen(function* () {
179
+ const name = options.name ?? repoSlug(options.url);
180
+ yield* Schema.decodeUnknown(RepoName)(name).pipe(Effect.mapError(() => new ReposConfigError({
181
+ path: MANIFEST_PATH,
182
+ reason: `invalid repo name "${name}": must be non-empty, contain no "/" or "\\", and not be "." or ".."`,
183
+ kind: "invalid"
184
+ })));
185
+ const manifest = yield* configStore.read(root).pipe(Effect.catchTag("ReposConfigError", (error) => error.kind === "missing" ? Effect.succeed({ repos: {} }) : Effect.fail(error)));
186
+ if (manifest.repos[name]) return yield* Effect.fail(new ReposConfigError({
187
+ path: MANIFEST_PATH,
188
+ reason: `"${name}" is already vendored — use pin to change its ref`,
189
+ kind: "invalid"
190
+ }));
191
+ const repoPath = `${REPOS_DIR}/${name}`;
192
+ const subPath = path.join(root, repoPath);
193
+ yield* runGit(root, [
194
+ "submodule",
195
+ "add",
196
+ "--depth",
197
+ "1",
198
+ options.url,
199
+ repoPath
200
+ ]);
201
+ yield* runGit(root, [
202
+ "config",
203
+ "-f",
204
+ ".gitmodules",
205
+ `submodule.${repoPath}.shallow`,
206
+ "true"
207
+ ]);
208
+ yield* fetchRef(subPath, options.ref);
209
+ yield* runGit(subPath, [
210
+ "checkout",
211
+ "--detach",
212
+ "FETCH_HEAD"
213
+ ]);
214
+ if (options.sparse && options.sparse.length > 0) yield* runGit(subPath, [
215
+ "sparse-checkout",
216
+ "set",
217
+ "--no-cone",
218
+ ...options.sparse
219
+ ]);
220
+ const entry = {
221
+ url: options.url,
222
+ ref: options.ref,
223
+ purpose: options.purpose,
224
+ ...options.sparse && options.sparse.length > 0 ? { sparse: options.sparse } : {}
225
+ };
226
+ yield* configStore.write(root, { repos: {
227
+ ...manifest.repos,
228
+ [name]: entry
229
+ } });
230
+ yield* runGit(root, [
231
+ "add",
232
+ ".gitmodules",
233
+ MANIFEST_PATH,
234
+ repoPath
235
+ ]);
236
+ return {
237
+ name,
238
+ ref: options.ref,
239
+ path: repoPath
240
+ };
241
+ });
242
+ const pin = (root, name, ref) => Effect.gen(function* () {
243
+ const manifest = yield* configStore.read(root);
244
+ const entry = manifest.repos[name];
245
+ if (!entry) return yield* Effect.fail(new RepoNotFoundError({ name }));
246
+ const repoPath = `${REPOS_DIR}/${name}`;
247
+ const subPath = path.join(root, repoPath);
248
+ const oldCommit = yield* runGit(subPath, ["rev-parse", "HEAD"]).pipe(Effect.orElseSucceed(() => null));
249
+ yield* fetchRef(subPath, ref);
250
+ yield* runGit(subPath, [
251
+ "checkout",
252
+ "--detach",
253
+ "FETCH_HEAD"
254
+ ]);
255
+ const newCommit = yield* runGit(subPath, ["rev-parse", "HEAD"]);
256
+ yield* configStore.write(root, { repos: {
257
+ ...manifest.repos,
258
+ [name]: {
259
+ ...entry,
260
+ ref
261
+ }
262
+ } });
263
+ yield* runGit(root, [
264
+ "add",
265
+ MANIFEST_PATH,
266
+ repoPath
267
+ ]);
268
+ const staleNoteIds = (entry.notes ?? []).filter((note) => note.ref !== ref).map((note) => note.id);
269
+ return {
270
+ name,
271
+ ref,
272
+ oldCommit,
273
+ newCommit,
274
+ commitMessage: `chore(repos): pin ${name} to ${ref}`,
275
+ staleNoteIds
276
+ };
277
+ });
278
+ const note = (root, name, op) => Effect.gen(function* () {
279
+ const manifest = yield* configStore.read(root);
280
+ const entry = manifest.repos[name];
281
+ if (!entry) return yield* Effect.fail(new RepoNotFoundError({ name }));
282
+ const notes = entry.notes ?? [];
283
+ if (op.op === "add") {
284
+ if (notes.length >= 10) return yield* Effect.fail(new ReposConfigError({
285
+ path: MANIFEST_PATH,
286
+ reason: `note limit (${10}) reached for ${name}; promote or remove notes first`,
287
+ kind: "invalid"
288
+ }));
289
+ const existingIds = new Set(notes.map((existing) => existing.id));
290
+ const hash = createHash("sha256").update(op.note).digest("hex");
291
+ let id;
292
+ for (let len = 4; len <= hash.length; len += 4) {
293
+ const candidate = `n-${hash.slice(0, len)}`;
294
+ if (!existingIds.has(candidate)) {
295
+ id = candidate;
296
+ break;
297
+ }
298
+ }
299
+ if (id === void 0) {
300
+ let counter = 2;
301
+ let candidate = `n-${hash}-${counter}`;
302
+ while (existingIds.has(candidate)) {
303
+ counter += 1;
304
+ candidate = `n-${hash}-${counter}`;
305
+ }
306
+ id = candidate;
307
+ }
308
+ const millis = yield* Clock.currentTimeMillis;
309
+ const date = new Date(millis).toISOString().slice(0, 10);
310
+ const newNote = {
311
+ id,
312
+ date,
313
+ ref: entry.ref,
314
+ note: op.note
315
+ };
316
+ const updatedNotes = [...notes, newNote];
317
+ const updatedEntry = {
318
+ ...entry,
319
+ notes: updatedNotes
320
+ };
321
+ yield* configStore.write(root, { repos: {
322
+ ...manifest.repos,
323
+ [name]: updatedEntry
324
+ } });
325
+ return {
326
+ name,
327
+ op: "add",
328
+ id,
329
+ noteCount: updatedNotes.length
330
+ };
331
+ }
332
+ const target = notes.find((existing) => existing.id === op.id);
333
+ if (!target) return yield* Effect.fail(new NoteNotFoundError({
334
+ name,
335
+ id: op.id
336
+ }));
337
+ const updatedNotes = notes.filter((existing) => existing.id !== op.id);
338
+ if (op.op === "remove") {
339
+ const updatedEntry = {
340
+ ...entry,
341
+ notes: updatedNotes
342
+ };
343
+ yield* configStore.write(root, { repos: {
344
+ ...manifest.repos,
345
+ [name]: updatedEntry
346
+ } });
347
+ return {
348
+ name,
349
+ op: "remove",
350
+ id: op.id,
351
+ noteCount: updatedNotes.length
352
+ };
353
+ }
354
+ const updatedOrientation = {
355
+ ...entry.orientation,
356
+ [op.into]: target.note
357
+ };
358
+ const updatedEntry = {
359
+ ...entry,
360
+ orientation: updatedOrientation,
361
+ notes: updatedNotes
362
+ };
363
+ yield* configStore.write(root, { repos: {
364
+ ...manifest.repos,
365
+ [name]: updatedEntry
366
+ } });
367
+ return {
368
+ name,
369
+ op: "promote",
370
+ id: op.id,
371
+ noteCount: updatedNotes.length
372
+ };
373
+ });
374
+ return ReposManager.of({
375
+ status,
376
+ sync,
377
+ add,
378
+ pin,
379
+ note
380
+ });
381
+ }));
382
+
383
+ //#endregion
384
+ export { ReposManager, ReposManagerBase, ReposManagerLive, STALE_LOCK_MAX_AGE_MS };