@savvy-web/silk-effects 3.2.5 → 3.3.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.
- package/index.d.ts +346 -6
- package/index.js +2 -1
- package/lint/cli/templates/markdownlint.gen.js +2 -1
- package/lint/handlers/ShellScripts.js +8 -3
- package/package.json +1 -1
- package/repos/constants.js +20 -0
- package/repos/errors.js +50 -0
- package/repos/index.js +43 -0
- package/repos/schemas/manifest.js +73 -0
- package/repos/schemas/reports.js +71 -0
- package/repos/services/config-store.js +86 -0
- package/repos/services/manager.js +384 -0
package/index.d.ts
CHANGED
|
@@ -7484,11 +7484,16 @@ declare class PnpmWorkspace {
|
|
|
7484
7484
|
/**
|
|
7485
7485
|
* Handler for shell script files.
|
|
7486
7486
|
*
|
|
7487
|
-
* Removes executable bit by default
|
|
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
|
-
*
|
|
7491
|
-
*
|
|
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$
|
|
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$
|
|
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 };
|
|
@@ -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
|
|
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
|
-
*
|
|
11
|
-
*
|
|
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
|
@@ -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 };
|
package/repos/errors.js
ADDED
|
@@ -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 };
|