@savvy-web/silk-effects 5.3.1 → 5.5.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/README.md +28 -1
- package/index.d.ts +251 -7
- package/package.json +4 -4
- package/repos/errors.js +12 -1
- package/repos/index.js +18 -4
- package/repos/schemas/drift.js +49 -0
- package/repos/schemas/reports.js +74 -3
- package/repos/services/config-store.js +36 -2
- package/repos/services/drift.js +168 -0
- package/repos/services/lockdown.js +119 -0
- package/repos/services/manager.js +384 -71
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Shared [Effect](https://effect.website/) library providing Silk Suite convention
|
|
|
13
13
|
- Supply the Silk commitlint config, its prompt and formatter, and the `silk/body-no-markdown` rule
|
|
14
14
|
- Run the lint-staged handlers and the `savvy lint fmt` formatters from a single implementation so the hook and the CLI cannot drift
|
|
15
15
|
- Inspect a Turborepo read-only — diagnose per-package cache hits, derive the task graph and compute affected packages, all over `turbo --dry`
|
|
16
|
-
- Manage the vendored reference repos declared in `.repos/config.json
|
|
16
|
+
- Manage the vendored reference repos declared in `.repos/config.json` — submodule status, drift detection, sync, add, pin, note, remove, rename and restore — and keep every vendored tree read-only between mutations
|
|
17
17
|
- Locate config files and keep Biome schema URLs in sync across workspaces
|
|
18
18
|
|
|
19
19
|
## Install
|
|
@@ -364,6 +364,33 @@ const diagnosis = await Effect.runPromise(
|
|
|
364
364
|
// => CacheDiagnosis: per-package HIT/MISS breakdown for the task
|
|
365
365
|
```
|
|
366
366
|
|
|
367
|
+
#### ReposManager, ReposDrift and ReposLockdown
|
|
368
|
+
|
|
369
|
+
The `Repos` namespace drives vendored reference repos — upstream sources checked out as git submodules under `.repos/`, declared in a `.repos/config.json` manifest. `ReposConfigStore` reads and writes that manifest, and `ReposManager` does the git work: `status(root)` reports presence, the pinned commit, working-tree dirtiness and stale notes, `sync(root)` initializes missing submodules and applies each entry's sparse-checkout, `pin(root, name, ref)` moves an entry to a new ref, `add(root, options)` vendors a new one, `note(root, name, op)` adds, removes or promotes an agent note, `remove(root, name)` unvendors an entry, `rename(root, oldName, newName)` renames one in place, and `restore(root, names?)` hard-resets one or more dirty checkouts back to their pinned commit. `ReposDrift.check(root)` is a read-only companion — it reconciles the manifest, `.gitmodules`, the worktree, and `git submodule status`, reporting any mismatch as a typed drift kind.
|
|
370
|
+
|
|
371
|
+
`ReposLockdown` is the permissions boundary around all of that. `lock(root, name)` chmods a vendored worktree and its submodule git metadata to files `0444` and directories `0555` (an executable file locks at `0555` instead, preserving its executable bit); `unlock` reverses it, restoring `0644`/`0755` (`0755` for a file that was executable); `withUnlocked(root, name, effect)` brackets an effect between the two. `ReposManager`'s `sync`, `add`, `pin`, `remove` and `restore` run their git mutations inside that bracket and re-lock afterwards, and `rename` hand-rolls the same unlock/relock contract across its `oldName`→`newName` move, so a vendored tree is read-only whenever the manager is not mid-write. Reads are unaffected — a locked tree needs no special handling to open a file.
|
|
372
|
+
|
|
373
|
+
Two consequences for callers: `ReposManager.layer` requires `ReposLockdown` alongside `ReposConfigStore`, `Git`, `FileSystem` and `Path`, and `sync`, `add`, `pin`, `remove`, `rename` and `restore` widen their error channel with `ReposLockdownError`, which carries the offending `path` and a `reason`.
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
import { Effect, Layer } from "effect";
|
|
377
|
+
import { NodeServices } from "@effect/platform-node";
|
|
378
|
+
import { Git } from "@effected/git";
|
|
379
|
+
import { Repos } from "@savvy-web/silk-effects";
|
|
380
|
+
|
|
381
|
+
const report = await Effect.runPromise(
|
|
382
|
+
Effect.gen(function* () {
|
|
383
|
+
const repos = yield* Repos.ReposManager;
|
|
384
|
+
return yield* repos.sync(process.cwd());
|
|
385
|
+
}).pipe(
|
|
386
|
+
Effect.provide(Repos.ReposManager.layer),
|
|
387
|
+
Effect.provide(Layer.mergeAll(Repos.ReposConfigStore.layer, Repos.ReposLockdown.layer, Git.layer)),
|
|
388
|
+
Effect.provide(NodeServices.layer),
|
|
389
|
+
),
|
|
390
|
+
);
|
|
391
|
+
// => ReposSyncReport: per-repo initialization and sparse-checkout outcome, trees left read-only
|
|
392
|
+
```
|
|
393
|
+
|
|
367
394
|
## Documentation
|
|
368
395
|
|
|
369
396
|
- [Overview](./docs/01-overview.md) — what the library is, its design philosophy and platform-layer model
|
package/index.d.ts
CHANGED
|
@@ -8281,6 +8281,60 @@ declare class NoteNotFoundError extends NoteNotFoundErrorBase<{
|
|
|
8281
8281
|
}> {
|
|
8282
8282
|
get message(): string;
|
|
8283
8283
|
}
|
|
8284
|
+
/** @internal */
|
|
8285
|
+
declare const ReposLockdownErrorBase: 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 & {
|
|
8286
|
+
readonly _tag: "ReposLockdownError";
|
|
8287
|
+
} & Readonly<A>;
|
|
8288
|
+
/**
|
|
8289
|
+
* A permissions (lockdown) operation on a vendored repo failed.
|
|
8290
|
+
* @public
|
|
8291
|
+
*/
|
|
8292
|
+
declare class ReposLockdownError extends ReposLockdownErrorBase<{
|
|
8293
|
+
readonly path: string;
|
|
8294
|
+
readonly reason: string;
|
|
8295
|
+
}> {
|
|
8296
|
+
get message(): string;
|
|
8297
|
+
}
|
|
8298
|
+
//#endregion
|
|
8299
|
+
//#region src/repos/schemas/drift.d.ts
|
|
8300
|
+
/**
|
|
8301
|
+
* The kinds of drift {@link ReposDrift} can detect between the four
|
|
8302
|
+
* authorities it reconciles: the manifest, `.gitmodules`, the worktree, and
|
|
8303
|
+
* `git submodule status`.
|
|
8304
|
+
* @public
|
|
8305
|
+
*/
|
|
8306
|
+
declare const DriftKind: Schema.Literals<readonly ["urlMismatch", "pathMismatch", "unregisteredManifestEntry", "orphanGitmodulesEntry", "missingWorktree", "checkoutDiverged", "missingShallow", "gitmodulesUnparsable"]>;
|
|
8307
|
+
/** @public */
|
|
8308
|
+
type DriftKind = typeof DriftKind.Type;
|
|
8309
|
+
declare const RepoDrift_base: Schema.Class<RepoDrift, Schema.Struct<{
|
|
8310
|
+
readonly name: Schema.String;
|
|
8311
|
+
readonly kind: Schema.Literals<readonly ["urlMismatch", "pathMismatch", "unregisteredManifestEntry", "orphanGitmodulesEntry", "missingWorktree", "checkoutDiverged", "missingShallow", "gitmodulesUnparsable"]>;
|
|
8312
|
+
readonly detail: Schema.String;
|
|
8313
|
+
readonly manifestValue: Schema.optionalKey<Schema.String>;
|
|
8314
|
+
readonly observedValue: Schema.optionalKey<Schema.String>;
|
|
8315
|
+
}>, {}>;
|
|
8316
|
+
/**
|
|
8317
|
+
* One detected disagreement between two (or more) of the four authorities
|
|
8318
|
+
* for a single vendored repo.
|
|
8319
|
+
*
|
|
8320
|
+
* @remarks
|
|
8321
|
+
* `manifestValue`/`observedValue` carry the two disagreeing values when the
|
|
8322
|
+
* drift is a value mismatch (`urlMismatch`, `pathMismatch`); other kinds
|
|
8323
|
+
* populate whichever side has a value to show (or neither, for
|
|
8324
|
+
* `gitmodulesUnparsable`, which is not about one repo).
|
|
8325
|
+
* @public
|
|
8326
|
+
*/
|
|
8327
|
+
declare class RepoDrift extends RepoDrift_base {}
|
|
8328
|
+
declare const ReposDriftReport_base: Schema.Class<ReposDriftReport, Schema.Struct<{
|
|
8329
|
+
readonly drifts: Schema.$Array<typeof RepoDrift>;
|
|
8330
|
+
readonly clean: Schema.Boolean;
|
|
8331
|
+
}>, {}>;
|
|
8332
|
+
/**
|
|
8333
|
+
* The result of reconciling the manifest, `.gitmodules`, the worktree, and
|
|
8334
|
+
* `git submodule status` for every vendored repo.
|
|
8335
|
+
* @public
|
|
8336
|
+
*/
|
|
8337
|
+
declare class ReposDriftReport extends ReposDriftReport_base {}
|
|
8284
8338
|
//#endregion
|
|
8285
8339
|
//#region src/repos/schemas/manifest.d.ts
|
|
8286
8340
|
/**
|
|
@@ -8368,6 +8422,21 @@ type ReposManifestFile = typeof ReposManifestFile.Type;
|
|
|
8368
8422
|
/**
|
|
8369
8423
|
* Status of one vendored repo: gitlink presence and dirtiness, plus notes
|
|
8370
8424
|
* that no longer match the pinned ref.
|
|
8425
|
+
*
|
|
8426
|
+
* @remarks
|
|
8427
|
+
* `commit` is an alias of `stagedCommit`, retained for one release so an
|
|
8428
|
+
* existing consumer reading `entry.commit` keeps working while it migrates to
|
|
8429
|
+
* the index-aware triple. It carries no independent release tag beyond
|
|
8430
|
+
* `@public` (this schema has no narrower audience to gate it behind).
|
|
8431
|
+
* Removal is scheduled by a tracked issue, not gated on a renderer migration
|
|
8432
|
+
* precondition here.
|
|
8433
|
+
*
|
|
8434
|
+
* Not fully behavior-preserving: `commit: stagedCommit ?? null` reads `null`
|
|
8435
|
+
* for a gitlink committed at `HEAD` but staged for REMOVAL, where the prior
|
|
8436
|
+
* single-`commit` field showed the committed oid. `stagedCommit` is `None`
|
|
8437
|
+
* in exactly that case (nothing is staged), so the alias reports "nothing
|
|
8438
|
+
* staged" rather than "here is what HEAD still has" — a real, if narrow,
|
|
8439
|
+
* difference from the pre-triple `commit` field's behavior.
|
|
8371
8440
|
* @public
|
|
8372
8441
|
*/
|
|
8373
8442
|
declare const RepoStatusEntry: Schema.Struct<{
|
|
@@ -8375,7 +8444,14 @@ declare const RepoStatusEntry: Schema.Struct<{
|
|
|
8375
8444
|
readonly ref: Schema.String;
|
|
8376
8445
|
readonly purpose: Schema.String;
|
|
8377
8446
|
readonly present: Schema.Boolean;
|
|
8447
|
+
/** @deprecated alias of `stagedCommit`; retained for one release. */
|
|
8378
8448
|
readonly commit: Schema.NullOr<Schema.String>;
|
|
8449
|
+
/** The gitlink oid staged in the index (`git ls-files --stage`) — sees a pin BEFORE it is committed. */
|
|
8450
|
+
readonly stagedCommit: Schema.optionalKey<Schema.String>;
|
|
8451
|
+
/** The gitlink oid committed at `HEAD` (`git ls-tree HEAD`). */
|
|
8452
|
+
readonly committedCommit: Schema.optionalKey<Schema.String>;
|
|
8453
|
+
/** The commit actually checked out in the submodule worktree (`git rev-parse HEAD` inside it); absent when there is no checkout. */
|
|
8454
|
+
readonly checkedOutCommit: Schema.optionalKey<Schema.String>;
|
|
8379
8455
|
readonly dirty: Schema.Boolean;
|
|
8380
8456
|
readonly staleNoteIds: Schema.$Array<Schema.String>;
|
|
8381
8457
|
}>;
|
|
@@ -8391,7 +8467,14 @@ declare const ReposStatusReport: Schema.Struct<{
|
|
|
8391
8467
|
readonly ref: Schema.String;
|
|
8392
8468
|
readonly purpose: Schema.String;
|
|
8393
8469
|
readonly present: Schema.Boolean;
|
|
8470
|
+
/** @deprecated alias of `stagedCommit`; retained for one release. */
|
|
8394
8471
|
readonly commit: Schema.NullOr<Schema.String>;
|
|
8472
|
+
/** The gitlink oid staged in the index (`git ls-files --stage`) — sees a pin BEFORE it is committed. */
|
|
8473
|
+
readonly stagedCommit: Schema.optionalKey<Schema.String>;
|
|
8474
|
+
/** The gitlink oid committed at `HEAD` (`git ls-tree HEAD`). */
|
|
8475
|
+
readonly committedCommit: Schema.optionalKey<Schema.String>;
|
|
8476
|
+
/** The commit actually checked out in the submodule worktree (`git rev-parse HEAD` inside it); absent when there is no checkout. */
|
|
8477
|
+
readonly checkedOutCommit: Schema.optionalKey<Schema.String>;
|
|
8395
8478
|
readonly dirty: Schema.Boolean;
|
|
8396
8479
|
readonly staleNoteIds: Schema.$Array<Schema.String>;
|
|
8397
8480
|
}>>;
|
|
@@ -8402,7 +8485,8 @@ type ReposStatusReport = typeof ReposStatusReport.Type;
|
|
|
8402
8485
|
/**
|
|
8403
8486
|
* Result of reconciling working-tree submodules with the manifest: missing
|
|
8404
8487
|
* repos initialized, sparse-checkout patterns re-applied, already-present
|
|
8405
|
-
* repos left alone,
|
|
8488
|
+
* repos left alone, stale locks cleared, drifted submodule URLs reconciled,
|
|
8489
|
+
* and orphan manifest entries (no gitlink at all) registered.
|
|
8406
8490
|
* @public
|
|
8407
8491
|
*/
|
|
8408
8492
|
declare const ReposSyncReport: Schema.Struct<{
|
|
@@ -8410,6 +8494,8 @@ declare const ReposSyncReport: Schema.Struct<{
|
|
|
8410
8494
|
readonly sparseApplied: Schema.$Array<Schema.String>;
|
|
8411
8495
|
readonly upToDate: Schema.$Array<Schema.String>;
|
|
8412
8496
|
readonly clearedLocks: Schema.$Array<Schema.String>;
|
|
8497
|
+
readonly urlSynced: Schema.$Array<Schema.String>;
|
|
8498
|
+
readonly registered: Schema.$Array<Schema.String>;
|
|
8413
8499
|
}>;
|
|
8414
8500
|
/** @public */
|
|
8415
8501
|
type ReposSyncReport = typeof ReposSyncReport.Type;
|
|
@@ -8438,6 +8524,62 @@ declare const ReposAddResult: Schema.Struct<{
|
|
|
8438
8524
|
}>;
|
|
8439
8525
|
/** @public */
|
|
8440
8526
|
type ReposAddResult = typeof ReposAddResult.Type;
|
|
8527
|
+
/**
|
|
8528
|
+
* Result of removing a vendored repo from the manifest: the gitlink, module
|
|
8529
|
+
* gitdir, and `.gitmodules` section are all gone, and the entry's notes are
|
|
8530
|
+
* surfaced so any durable ones can be promoted elsewhere before this result
|
|
8531
|
+
* is committed.
|
|
8532
|
+
* @public
|
|
8533
|
+
*/
|
|
8534
|
+
declare const ReposRemoveResult: Schema.Struct<{
|
|
8535
|
+
readonly name: Schema.String;
|
|
8536
|
+
readonly path: Schema.String;
|
|
8537
|
+
readonly commitMessage: Schema.String;
|
|
8538
|
+
readonly removedNotes: Schema.$Array<Schema.Struct<{
|
|
8539
|
+
readonly id: Schema.String;
|
|
8540
|
+
readonly date: Schema.String;
|
|
8541
|
+
readonly ref: Schema.String;
|
|
8542
|
+
readonly note: Schema.String;
|
|
8543
|
+
}>>;
|
|
8544
|
+
}>;
|
|
8545
|
+
/** @public */
|
|
8546
|
+
type ReposRemoveResult = typeof ReposRemoveResult.Type;
|
|
8547
|
+
/**
|
|
8548
|
+
* Result of renaming a vendored repo's manifest key: the `.repos/<name>`
|
|
8549
|
+
* worktree moved, the module gitdir's `core.worktree` values re-pointed, the
|
|
8550
|
+
* `.gitmodules` section canonicalized to the new name, and the manifest key
|
|
8551
|
+
* renamed.
|
|
8552
|
+
* @public
|
|
8553
|
+
*/
|
|
8554
|
+
declare const ReposRenameResult: Schema.Struct<{
|
|
8555
|
+
readonly oldName: Schema.String;
|
|
8556
|
+
readonly newName: Schema.String;
|
|
8557
|
+
readonly path: Schema.String;
|
|
8558
|
+
readonly commitMessage: Schema.String;
|
|
8559
|
+
}>;
|
|
8560
|
+
/** @public */
|
|
8561
|
+
type ReposRenameResult = typeof ReposRenameResult.Type;
|
|
8562
|
+
/**
|
|
8563
|
+
* Result of hard-resetting one or more vendored repos to their staged (or
|
|
8564
|
+
* committed) gitlink commit and re-applying sparse-checkout paths.
|
|
8565
|
+
*
|
|
8566
|
+
* @remarks
|
|
8567
|
+
* `restored` lists every repo actually reset, paired with the commit it was
|
|
8568
|
+
* reset to. `skippedClean` is populated ONLY by the names-omitted form of
|
|
8569
|
+
* {@link ReposManagerShape.restore} — repos left untouched because `status`
|
|
8570
|
+
* reported them clean; an explicit-names call never skips anything (an
|
|
8571
|
+
* explicit ask is always honored), so it always reports an empty array.
|
|
8572
|
+
* @public
|
|
8573
|
+
*/
|
|
8574
|
+
declare const ReposRestoreResult: Schema.Struct<{
|
|
8575
|
+
readonly restored: Schema.$Array<Schema.Struct<{
|
|
8576
|
+
readonly name: Schema.String;
|
|
8577
|
+
readonly commit: Schema.String;
|
|
8578
|
+
}>>;
|
|
8579
|
+
readonly skippedClean: Schema.$Array<Schema.String>;
|
|
8580
|
+
}>;
|
|
8581
|
+
/** @public */
|
|
8582
|
+
type ReposRestoreResult = typeof ReposRestoreResult.Type;
|
|
8441
8583
|
/**
|
|
8442
8584
|
* Result of an agent-note mutation against a vendored repo.
|
|
8443
8585
|
* @public
|
|
@@ -8460,6 +8602,14 @@ interface ReposConfigStoreShape {
|
|
|
8460
8602
|
readonly exists: (root: string) => Effect.Effect<boolean>;
|
|
8461
8603
|
readonly read: (root: string) => Effect.Effect<ReposManifestFile, ReposConfigError>;
|
|
8462
8604
|
readonly write: (root: string, manifest: ReposManifestFile) => Effect.Effect<void, ReposConfigError>;
|
|
8605
|
+
/**
|
|
8606
|
+
* Serialized read-modify-write: acquires an exclusive-create lock file
|
|
8607
|
+
* beside the manifest, reads the current manifest (an absent manifest is
|
|
8608
|
+
* passed to `fn` as `{ repos: {} }` — `update` can initialize), runs `fn`,
|
|
8609
|
+
* writes the result, and always releases the lock. Concurrent callers
|
|
8610
|
+
* queue behind the lock rather than racing a lost update.
|
|
8611
|
+
*/
|
|
8612
|
+
readonly update: (root: string, fn: (manifest: ReposManifestFile) => ReposManifestFile | Effect.Effect<ReposManifestFile, ReposConfigError>) => Effect.Effect<ReposManifestFile, ReposConfigError>;
|
|
8463
8613
|
}
|
|
8464
8614
|
declare const ReposConfigStore_base: Context.ServiceClass<ReposConfigStore, "@savvy-web/silk-effects/ReposConfigStore", ReposConfigStoreShape>;
|
|
8465
8615
|
/**
|
|
@@ -8474,6 +8624,77 @@ declare class ReposConfigStore extends ReposConfigStore_base {
|
|
|
8474
8624
|
static readonly layer: Layer.Layer<ReposConfigStore, never, FileSystem.FileSystem | Path.Path>;
|
|
8475
8625
|
}
|
|
8476
8626
|
//#endregion
|
|
8627
|
+
//#region src/repos/services/drift.d.ts
|
|
8628
|
+
/**
|
|
8629
|
+
* The {@link ReposDrift} service shape.
|
|
8630
|
+
* @public
|
|
8631
|
+
*/
|
|
8632
|
+
interface ReposDriftShape {
|
|
8633
|
+
readonly check: (root: string) => Effect.Effect<ReposDriftReport, ReposConfigError | GitSubmoduleError>;
|
|
8634
|
+
}
|
|
8635
|
+
declare const ReposDrift_base: Context.ServiceClass<ReposDrift, "@savvy-web/silk-effects/ReposDrift", ReposDriftShape>;
|
|
8636
|
+
/**
|
|
8637
|
+
* Reconciles the four authorities a vendored repo's state is spread across —
|
|
8638
|
+
* the manifest, `.gitmodules`, the worktree, and `git submodule status` —
|
|
8639
|
+
* and reports every disagreement found. Read-only: no staging, no lockdown
|
|
8640
|
+
* interaction, so it runs unmodified against a locked (`ReposLockdown`)
|
|
8641
|
+
* tree.
|
|
8642
|
+
* @public
|
|
8643
|
+
*/
|
|
8644
|
+
declare class ReposDrift extends ReposDrift_base {
|
|
8645
|
+
/**
|
|
8646
|
+
* Production implementation of {@link ReposDrift}.
|
|
8647
|
+
* @public
|
|
8648
|
+
*/
|
|
8649
|
+
static readonly layer: Layer.Layer<ReposDrift, never, ReposConfigStore | Git | FileSystem.FileSystem | Path.Path>;
|
|
8650
|
+
}
|
|
8651
|
+
//#endregion
|
|
8652
|
+
//#region src/repos/services/lockdown.d.ts
|
|
8653
|
+
/**
|
|
8654
|
+
* The {@link ReposLockdown} service shape.
|
|
8655
|
+
* @public
|
|
8656
|
+
*/
|
|
8657
|
+
interface ReposLockdownShape {
|
|
8658
|
+
readonly lock: (root: string, name: string) => Effect.Effect<void, ReposLockdownError>;
|
|
8659
|
+
readonly unlock: (root: string, name: string) => Effect.Effect<void, ReposLockdownError>;
|
|
8660
|
+
readonly withUnlocked: <A, E, R>(root: string, name: string, effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | ReposLockdownError, R>;
|
|
8661
|
+
}
|
|
8662
|
+
/**
|
|
8663
|
+
* Derives a submodule's git metadata directory (`.git/modules/...`) from the
|
|
8664
|
+
* checkout itself rather than assuming it is named after the manifest key.
|
|
8665
|
+
*
|
|
8666
|
+
* A submodule worktree's `<root>/.repos/<name>/.git` is normally a FILE
|
|
8667
|
+
* containing a single `gitdir: <path>` line pointing at the real metadata
|
|
8668
|
+
* directory, which git names after whatever path/name the submodule was
|
|
8669
|
+
* REGISTERED under — not necessarily the manifest key (e.g. this repo's own
|
|
8670
|
+
* `effect` entry has gitdir `.git/modules/.repos/effect-smol`). This helper
|
|
8671
|
+
* reads that pointer and resolves it (relative pointers are relative to the
|
|
8672
|
+
* directory containing the `.git` file) so callers always land on the real
|
|
8673
|
+
* metadata directory.
|
|
8674
|
+
*
|
|
8675
|
+
* Falls back to the name-based path `<root>/.git/modules/<REPOS_DIR>/<name>`
|
|
8676
|
+
* whenever the pointer can't be read (submodule not initialized, `.git`
|
|
8677
|
+
* missing) — this never fails, it only degrades to prior behavior. If
|
|
8678
|
+
* `<root>/.repos/<name>/.git` is itself a directory (a plain, non-submodule
|
|
8679
|
+
* checkout), it is used directly as the metadata directory.
|
|
8680
|
+
*
|
|
8681
|
+
* @internal
|
|
8682
|
+
*/
|
|
8683
|
+
declare const resolveModuleDir: (fs: FileSystem.FileSystem, path: Path.Path, root: string, name: string) => Effect.Effect<string>;
|
|
8684
|
+
declare const ReposLockdown_base: Context.ServiceClass<ReposLockdown, "@savvy-web/silk-effects/ReposLockdown", ReposLockdownShape>;
|
|
8685
|
+
/**
|
|
8686
|
+
* Enforces OS-level read-only permissions on vendored repos so they cannot
|
|
8687
|
+
* be accidentally edited outside the sync flow.
|
|
8688
|
+
* @public
|
|
8689
|
+
*/
|
|
8690
|
+
declare class ReposLockdown extends ReposLockdown_base {
|
|
8691
|
+
/**
|
|
8692
|
+
* Production layer over the core FileSystem.
|
|
8693
|
+
* @public
|
|
8694
|
+
*/
|
|
8695
|
+
static readonly layer: Layer.Layer<ReposLockdown, never, FileSystem.FileSystem | Path.Path>;
|
|
8696
|
+
}
|
|
8697
|
+
//#endregion
|
|
8477
8698
|
//#region src/repos/services/manager.d.ts
|
|
8478
8699
|
/**
|
|
8479
8700
|
* Minimum age (in milliseconds) a `.lock` file must reach before `sync` will
|
|
@@ -8498,15 +8719,15 @@ declare const STALE_LOCK_MAX_AGE_MS: number;
|
|
|
8498
8719
|
*/
|
|
8499
8720
|
interface ReposManagerShape {
|
|
8500
8721
|
readonly status: (root: string) => Effect.Effect<ReposStatusReport, ReposConfigError | GitSubmoduleError>;
|
|
8501
|
-
readonly sync: (root: string) => Effect.Effect<ReposSyncReport, ReposConfigError | GitSubmoduleError>;
|
|
8722
|
+
readonly sync: (root: string) => Effect.Effect<ReposSyncReport, ReposConfigError | GitSubmoduleError | ReposLockdownError>;
|
|
8502
8723
|
readonly add: (root: string, options: {
|
|
8503
8724
|
readonly url: string;
|
|
8504
8725
|
readonly ref: string;
|
|
8505
8726
|
readonly purpose: string;
|
|
8506
8727
|
readonly name?: string;
|
|
8507
8728
|
readonly sparse?: ReadonlyArray<string>;
|
|
8508
|
-
}) => Effect.Effect<ReposAddResult, ReposConfigError | GitSubmoduleError>;
|
|
8509
|
-
readonly pin: (root: string, name: string, ref: string) => Effect.Effect<ReposPinResult, ReposConfigError | GitSubmoduleError | RepoNotFoundError>;
|
|
8729
|
+
}) => Effect.Effect<ReposAddResult, ReposConfigError | GitSubmoduleError | ReposLockdownError>;
|
|
8730
|
+
readonly pin: (root: string, name: string, ref: string) => Effect.Effect<ReposPinResult, ReposConfigError | GitSubmoduleError | RepoNotFoundError | ReposLockdownError>;
|
|
8510
8731
|
readonly note: (root: string, name: string, op: {
|
|
8511
8732
|
readonly op: "add";
|
|
8512
8733
|
readonly note: string;
|
|
@@ -8518,13 +8739,36 @@ interface ReposManagerShape {
|
|
|
8518
8739
|
readonly id: string;
|
|
8519
8740
|
readonly into: "layout" | "startHere";
|
|
8520
8741
|
}) => Effect.Effect<ReposNoteResult, ReposConfigError | RepoNotFoundError | NoteNotFoundError>;
|
|
8742
|
+
readonly remove: (root: string, name: string) => Effect.Effect<ReposRemoveResult, ReposConfigError | GitSubmoduleError | RepoNotFoundError | ReposLockdownError>;
|
|
8743
|
+
/**
|
|
8744
|
+
* Crash contract: unlike {@link add}, `rename` has no compensating
|
|
8745
|
+
* rollback. `Effect.uninterruptibleMask` guards fiber interruption, not a
|
|
8746
|
+
* hard process kill, so a `kill -9` between `git mv` and the manifest
|
|
8747
|
+
* write can leave the tree renamed in git, the manifest still holding
|
|
8748
|
+
* the old key, and the tree unlocked (the relock finalizer never runs).
|
|
8749
|
+
* This is a deliberate asymmetry with `add`, which DOES roll back —
|
|
8750
|
+
* `rename` does not, because unlike a fresh vendor there is no "nothing
|
|
8751
|
+
* happened yet" state to unwind back to.
|
|
8752
|
+
*
|
|
8753
|
+
* Recovery is NOT a guaranteed clean "just run it again": `git mv` is not
|
|
8754
|
+
* idempotent (a real-git probe confirms a second `git mv <old> <new>`
|
|
8755
|
+
* after the first already succeeded fails with "bad source"), so a crash
|
|
8756
|
+
* after `git mv` lands makes the next `rename` call fail at that same
|
|
8757
|
+
* step rather than resume past it. A crash in that window needs manual
|
|
8758
|
+
* inspection (`git status`, `.repos/config.json`, `.gitmodules`) before
|
|
8759
|
+
* retrying, not a blind re-invocation.
|
|
8760
|
+
*/
|
|
8761
|
+
readonly rename: (root: string, oldName: string, newName: string) => Effect.Effect<ReposRenameResult, ReposConfigError | GitSubmoduleError | RepoNotFoundError | ReposLockdownError>;
|
|
8762
|
+
readonly restore: (root: string, names?: ReadonlyArray<string>) => Effect.Effect<ReposRestoreResult, ReposConfigError | GitSubmoduleError | RepoNotFoundError | ReposLockdownError>;
|
|
8521
8763
|
}
|
|
8522
8764
|
declare const ReposManager_base: Context.ServiceClass<ReposManager, "@savvy-web/silk-effects/ReposManager", ReposManagerShape>;
|
|
8523
8765
|
/**
|
|
8524
8766
|
* Drives the vendored `.repos/` submodules over git: reports status
|
|
8525
8767
|
* (presence, dirtiness, stale notes), reconciles the working tree with the
|
|
8526
8768
|
* manifest, vendors new entries (`add`), re-pins existing entries to a new
|
|
8527
|
-
* ref (`pin`),
|
|
8769
|
+
* ref (`pin`), adds/removes/promotes agent notes (`note`), unvendors
|
|
8770
|
+
* (`remove`), renames (`rename`), and explicitly hard-resets dirty
|
|
8771
|
+
* checkouts back to their pinned commit (`restore`).
|
|
8528
8772
|
* @public
|
|
8529
8773
|
*/
|
|
8530
8774
|
declare class ReposManager extends ReposManager_base {
|
|
@@ -8539,10 +8783,10 @@ declare class ReposManager extends ReposManager_base {
|
|
|
8539
8783
|
* this module's {@link GitSubmoduleError} to keep the declared error unions.
|
|
8540
8784
|
* @public
|
|
8541
8785
|
*/
|
|
8542
|
-
static readonly layer: Layer.Layer<ReposManager, never, ReposConfigStore | Git | FileSystem.FileSystem | Path.Path>;
|
|
8786
|
+
static readonly layer: Layer.Layer<ReposManager, never, ReposConfigStore | Git | FileSystem.FileSystem | Path.Path | ReposLockdown>;
|
|
8543
8787
|
}
|
|
8544
8788
|
declare namespace index_d_exports$3 {
|
|
8545
|
-
export { GitSubmoduleError, GitSubmoduleErrorBase, MANIFEST_PATH, NOTE_LIMIT, NoteNotFoundError, NoteNotFoundErrorBase, REPOS_DIR, RepoEntry, RepoName, RepoNotFoundError, RepoNotFoundErrorBase, RepoNote, RepoOrientation, RepoStatusEntry, ReposAddResult, ReposConfigError, ReposConfigErrorBase, ReposConfigStore, ReposConfigStoreShape, ReposManager, ReposManagerShape, ReposManifestFile, ReposNoteResult, ReposPinResult, ReposStatusReport, ReposSyncReport, STALE_LOCK_MAX_AGE_MS };
|
|
8789
|
+
export { DriftKind, GitSubmoduleError, GitSubmoduleErrorBase, MANIFEST_PATH, NOTE_LIMIT, NoteNotFoundError, NoteNotFoundErrorBase, REPOS_DIR, RepoDrift, RepoEntry, RepoName, RepoNotFoundError, RepoNotFoundErrorBase, RepoNote, RepoOrientation, RepoStatusEntry, ReposAddResult, ReposConfigError, ReposConfigErrorBase, ReposConfigStore, ReposConfigStoreShape, ReposDrift, ReposDriftReport, ReposDriftShape, ReposLockdown, ReposLockdownError, ReposLockdownErrorBase, ReposLockdownShape, ReposManager, ReposManagerShape, ReposManifestFile, ReposNoteResult, ReposPinResult, ReposRemoveResult, ReposRenameResult, ReposRestoreResult, ReposStatusReport, ReposSyncReport, STALE_LOCK_MAX_AGE_MS, resolveModuleDir };
|
|
8546
8790
|
}
|
|
8547
8791
|
//#endregion
|
|
8548
8792
|
//#region src/schemas/BiomeConfig.d.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@savvy-web/silk-effects",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.5.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared Effect library for Silk Suite conventions",
|
|
6
6
|
"homepage": "https://github.com/savvy-web/systems/tree/main/packages/silk-effects",
|
|
@@ -33,14 +33,14 @@
|
|
|
33
33
|
"@changesets/config": "^4.0.0-next.6",
|
|
34
34
|
"@changesets/get-github-info": "^1.0.0-next.4",
|
|
35
35
|
"@changesets/get-release-plan": "^5.0.0-next.9",
|
|
36
|
-
"@effected/commands": "^0.
|
|
37
|
-
"@effected/git": "^0.
|
|
36
|
+
"@effected/commands": "^0.3.1",
|
|
37
|
+
"@effected/git": "^0.6.0",
|
|
38
38
|
"@effected/glob": "^0.2.2",
|
|
39
39
|
"@effected/jsonc": "^0.5.2",
|
|
40
40
|
"@effected/package-json": "^0.7.3",
|
|
41
41
|
"@effected/templates": "^0.1.1",
|
|
42
42
|
"@effected/walker": "^0.3.4",
|
|
43
|
-
"@effected/workspaces": "^0.
|
|
43
|
+
"@effected/workspaces": "^0.10.2",
|
|
44
44
|
"@effected/yaml": "^0.6.1",
|
|
45
45
|
"@manypkg/get-packages": "^3.1.0",
|
|
46
46
|
"mdast-util-heading-range": "^4.0.0",
|
package/repos/errors.js
CHANGED
|
@@ -45,6 +45,17 @@ var NoteNotFoundError = class extends NoteNotFoundErrorBase {
|
|
|
45
45
|
return `no note "${this.id}" on vendored repo "${this.name}"`;
|
|
46
46
|
}
|
|
47
47
|
};
|
|
48
|
+
/** @internal */
|
|
49
|
+
const ReposLockdownErrorBase = Data.TaggedError("ReposLockdownError");
|
|
50
|
+
/**
|
|
51
|
+
* A permissions (lockdown) operation on a vendored repo failed.
|
|
52
|
+
* @public
|
|
53
|
+
*/
|
|
54
|
+
var ReposLockdownError = class extends ReposLockdownErrorBase {
|
|
55
|
+
get message() {
|
|
56
|
+
return `repos lockdown failed at ${this.path}: ${this.reason}`;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
48
59
|
|
|
49
60
|
//#endregion
|
|
50
|
-
export { GitSubmoduleError, GitSubmoduleErrorBase, NoteNotFoundError, NoteNotFoundErrorBase, RepoNotFoundError, RepoNotFoundErrorBase, ReposConfigError, ReposConfigErrorBase };
|
|
61
|
+
export { GitSubmoduleError, GitSubmoduleErrorBase, NoteNotFoundError, NoteNotFoundErrorBase, RepoNotFoundError, RepoNotFoundErrorBase, ReposConfigError, ReposConfigErrorBase, ReposLockdownError, ReposLockdownErrorBase };
|
package/repos/index.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { __exportAll } from "../_virtual/_rolldown/runtime.js";
|
|
2
2
|
import { MANIFEST_PATH, REPOS_DIR } from "./constants.js";
|
|
3
|
-
import { GitSubmoduleError, GitSubmoduleErrorBase, NoteNotFoundError, NoteNotFoundErrorBase, RepoNotFoundError, RepoNotFoundErrorBase, ReposConfigError, ReposConfigErrorBase } from "./errors.js";
|
|
3
|
+
import { GitSubmoduleError, GitSubmoduleErrorBase, NoteNotFoundError, NoteNotFoundErrorBase, RepoNotFoundError, RepoNotFoundErrorBase, ReposConfigError, ReposConfigErrorBase, ReposLockdownError, ReposLockdownErrorBase } from "./errors.js";
|
|
4
|
+
import { DriftKind, RepoDrift, ReposDriftReport } from "./schemas/drift.js";
|
|
4
5
|
import { RepoEntry, RepoName, RepoNote, RepoOrientation, ReposManifestFile } from "./schemas/manifest.js";
|
|
5
|
-
import { RepoStatusEntry, ReposAddResult, ReposNoteResult, ReposPinResult, ReposStatusReport, ReposSyncReport } from "./schemas/reports.js";
|
|
6
|
+
import { RepoStatusEntry, ReposAddResult, ReposNoteResult, ReposPinResult, ReposRemoveResult, ReposRenameResult, ReposRestoreResult, ReposStatusReport, ReposSyncReport } from "./schemas/reports.js";
|
|
6
7
|
import { ReposConfigStore } from "./services/config-store.js";
|
|
8
|
+
import { ReposDrift } from "./services/drift.js";
|
|
9
|
+
import { ReposLockdown, resolveModuleDir } from "./services/lockdown.js";
|
|
7
10
|
import { ReposManager, STALE_LOCK_MAX_AGE_MS } from "./services/manager.js";
|
|
8
11
|
|
|
9
12
|
//#region src/repos/index.ts
|
|
10
13
|
var repos_exports = /* @__PURE__ */ __exportAll({
|
|
14
|
+
DriftKind: () => DriftKind,
|
|
11
15
|
GitSubmoduleError: () => GitSubmoduleError,
|
|
12
16
|
GitSubmoduleErrorBase: () => GitSubmoduleErrorBase,
|
|
13
17
|
MANIFEST_PATH: () => MANIFEST_PATH,
|
|
@@ -15,6 +19,7 @@ var repos_exports = /* @__PURE__ */ __exportAll({
|
|
|
15
19
|
NoteNotFoundError: () => NoteNotFoundError,
|
|
16
20
|
NoteNotFoundErrorBase: () => NoteNotFoundErrorBase,
|
|
17
21
|
REPOS_DIR: () => REPOS_DIR,
|
|
22
|
+
RepoDrift: () => RepoDrift,
|
|
18
23
|
RepoEntry: () => RepoEntry,
|
|
19
24
|
RepoName: () => RepoName,
|
|
20
25
|
RepoNotFoundError: () => RepoNotFoundError,
|
|
@@ -26,14 +31,23 @@ var repos_exports = /* @__PURE__ */ __exportAll({
|
|
|
26
31
|
ReposConfigError: () => ReposConfigError,
|
|
27
32
|
ReposConfigErrorBase: () => ReposConfigErrorBase,
|
|
28
33
|
ReposConfigStore: () => ReposConfigStore,
|
|
34
|
+
ReposDrift: () => ReposDrift,
|
|
35
|
+
ReposDriftReport: () => ReposDriftReport,
|
|
36
|
+
ReposLockdown: () => ReposLockdown,
|
|
37
|
+
ReposLockdownError: () => ReposLockdownError,
|
|
38
|
+
ReposLockdownErrorBase: () => ReposLockdownErrorBase,
|
|
29
39
|
ReposManager: () => ReposManager,
|
|
30
40
|
ReposManifestFile: () => ReposManifestFile,
|
|
31
41
|
ReposNoteResult: () => ReposNoteResult,
|
|
32
42
|
ReposPinResult: () => ReposPinResult,
|
|
43
|
+
ReposRemoveResult: () => ReposRemoveResult,
|
|
44
|
+
ReposRenameResult: () => ReposRenameResult,
|
|
45
|
+
ReposRestoreResult: () => ReposRestoreResult,
|
|
33
46
|
ReposStatusReport: () => ReposStatusReport,
|
|
34
47
|
ReposSyncReport: () => ReposSyncReport,
|
|
35
|
-
STALE_LOCK_MAX_AGE_MS: () => STALE_LOCK_MAX_AGE_MS
|
|
48
|
+
STALE_LOCK_MAX_AGE_MS: () => STALE_LOCK_MAX_AGE_MS,
|
|
49
|
+
resolveModuleDir: () => resolveModuleDir
|
|
36
50
|
});
|
|
37
51
|
|
|
38
52
|
//#endregion
|
|
39
|
-
export { GitSubmoduleError, GitSubmoduleErrorBase, MANIFEST_PATH, NoteNotFoundError, NoteNotFoundErrorBase, REPOS_DIR, RepoEntry, RepoName, RepoNotFoundError, RepoNotFoundErrorBase, RepoNote, RepoOrientation, RepoStatusEntry, ReposAddResult, ReposConfigError, ReposConfigErrorBase, ReposConfigStore, ReposManager, ReposManifestFile, ReposNoteResult, ReposPinResult, ReposStatusReport, ReposSyncReport, STALE_LOCK_MAX_AGE_MS, repos_exports };
|
|
53
|
+
export { DriftKind, GitSubmoduleError, GitSubmoduleErrorBase, MANIFEST_PATH, NoteNotFoundError, NoteNotFoundErrorBase, REPOS_DIR, RepoDrift, RepoEntry, RepoName, RepoNotFoundError, RepoNotFoundErrorBase, RepoNote, RepoOrientation, RepoStatusEntry, ReposAddResult, ReposConfigError, ReposConfigErrorBase, ReposConfigStore, ReposDrift, ReposDriftReport, ReposLockdown, ReposLockdownError, ReposLockdownErrorBase, ReposManager, ReposManifestFile, ReposNoteResult, ReposPinResult, ReposRemoveResult, ReposRenameResult, ReposRestoreResult, ReposStatusReport, ReposSyncReport, STALE_LOCK_MAX_AGE_MS, repos_exports, resolveModuleDir };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/repos/schemas/drift.ts
|
|
4
|
+
/**
|
|
5
|
+
* The kinds of drift {@link ReposDrift} can detect between the four
|
|
6
|
+
* authorities it reconciles: the manifest, `.gitmodules`, the worktree, and
|
|
7
|
+
* `git submodule status`.
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
const DriftKind = Schema.Literals([
|
|
11
|
+
"urlMismatch",
|
|
12
|
+
"pathMismatch",
|
|
13
|
+
"unregisteredManifestEntry",
|
|
14
|
+
"orphanGitmodulesEntry",
|
|
15
|
+
"missingWorktree",
|
|
16
|
+
"checkoutDiverged",
|
|
17
|
+
"missingShallow",
|
|
18
|
+
"gitmodulesUnparsable"
|
|
19
|
+
]);
|
|
20
|
+
/**
|
|
21
|
+
* One detected disagreement between two (or more) of the four authorities
|
|
22
|
+
* for a single vendored repo.
|
|
23
|
+
*
|
|
24
|
+
* @remarks
|
|
25
|
+
* `manifestValue`/`observedValue` carry the two disagreeing values when the
|
|
26
|
+
* drift is a value mismatch (`urlMismatch`, `pathMismatch`); other kinds
|
|
27
|
+
* populate whichever side has a value to show (or neither, for
|
|
28
|
+
* `gitmodulesUnparsable`, which is not about one repo).
|
|
29
|
+
* @public
|
|
30
|
+
*/
|
|
31
|
+
var RepoDrift = class extends Schema.Class("RepoDrift")({
|
|
32
|
+
name: Schema.String,
|
|
33
|
+
kind: DriftKind,
|
|
34
|
+
detail: Schema.String,
|
|
35
|
+
manifestValue: Schema.optionalKey(Schema.String),
|
|
36
|
+
observedValue: Schema.optionalKey(Schema.String)
|
|
37
|
+
}) {};
|
|
38
|
+
/**
|
|
39
|
+
* The result of reconciling the manifest, `.gitmodules`, the worktree, and
|
|
40
|
+
* `git submodule status` for every vendored repo.
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
var ReposDriftReport = class extends Schema.Class("ReposDriftReport")({
|
|
44
|
+
drifts: Schema.Array(RepoDrift),
|
|
45
|
+
clean: Schema.Boolean
|
|
46
|
+
}) {};
|
|
47
|
+
|
|
48
|
+
//#endregion
|
|
49
|
+
export { DriftKind, RepoDrift, ReposDriftReport };
|