@savvy-web/silk-effects 3.3.1 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/changesets/changelog/getReleaseLine.js +1 -1
  2. package/changesets/constants.js +0 -18
  3. package/changesets/index.js +10 -24
  4. package/changesets/schemas/changeset.js +8 -4
  5. package/changesets/schemas/dependency-table.js +15 -4
  6. package/changesets/schemas/git.js +7 -2
  7. package/changesets/schemas/github.js +4 -4
  8. package/changesets/schemas/options.js +3 -3
  9. package/changesets/schemas/package-scope.js +2 -5
  10. package/changesets/schemas/primitives.js +2 -2
  11. package/changesets/schemas/release-plan.js +12 -8
  12. package/changesets/schemas/version-files.js +4 -4
  13. package/changesets/services/branch-analyzer.js +60 -191
  14. package/changesets/services/changelog.js +2 -15
  15. package/changesets/services/config-inspector.js +109 -75
  16. package/changesets/services/deps-regen.js +58 -32
  17. package/changesets/services/github.js +3 -16
  18. package/changesets/services/maintenance-reason.js +5 -1
  19. package/changesets/services/markdown.js +3 -16
  20. package/changesets/services/release-planner.js +4 -13
  21. package/changesets/utils/commit-parser.js +0 -18
  22. package/changesets/utils/dep-diff.js +0 -27
  23. package/changesets/utils/git.js +15 -50
  24. package/changesets/utils/issue-refs.js +0 -13
  25. package/changesets/utils/logger.js +0 -13
  26. package/changesets/utils/markdown-link.js +0 -14
  27. package/changesets/utils/publishability.js +11 -24
  28. package/changesets/utils/strip-frontmatter.js +0 -19
  29. package/changesets/utils/version-files.js +85 -51
  30. package/commitlint/config/schema.js +9 -5
  31. package/commitlint/detection/scopes.js +2 -7
  32. package/commitlint/formatter/messages.js +0 -5
  33. package/commitlint/hook/diagnostics/branch.js +12 -12
  34. package/commitlint/hook/diagnostics/open-issues.js +13 -7
  35. package/commitlint/hook/diagnostics/signing.js +30 -37
  36. package/commitlint/hook/envelope.js +2 -8
  37. package/commitlint/hook/silence-logger.js +14 -12
  38. package/index.d.ts +897 -932
  39. package/lint/cli/templates/markdownlint.gen.js +0 -9
  40. package/lint/handlers/PnpmWorkspace.js +26 -12
  41. package/lint/utils/Filter.js +0 -13
  42. package/lint/utils/Workspace.js +10 -6
  43. package/package.json +11 -10
  44. package/repos/index.js +3 -5
  45. package/repos/schemas/manifest.js +7 -13
  46. package/repos/schemas/reports.js +5 -1
  47. package/repos/services/config-store.js +6 -10
  48. package/repos/services/manager.js +52 -112
  49. package/schemas/CommentStyle.js +1 -1
  50. package/schemas/ResolvedTool.js +52 -17
  51. package/schemas/SectionBlock.js +1 -1
  52. package/schemas/SectionDefinition.js +2 -2
  53. package/schemas/TagStrategySchemas.js +1 -1
  54. package/schemas/ToolDefinition.js +1 -1
  55. package/schemas/ToolResults.js +1 -1
  56. package/schemas/VersioningSchemas.js +28 -9
  57. package/schemas/WorkspaceAnalysisSchemas.js +27 -17
  58. package/services/BiomeSchemaSync.js +7 -8
  59. package/services/ChangesetConfig.js +2 -2
  60. package/services/ChangesetConfigReader.js +7 -8
  61. package/services/ConfigDiscovery.js +5 -6
  62. package/services/ManagedSection.js +3 -4
  63. package/services/SilkPublishability.js +38 -15
  64. package/services/SilkWorkspaceAnalyzer.js +7 -9
  65. package/services/TagStrategy.js +3 -3
  66. package/services/ToolDiscovery.js +22 -21
  67. package/services/VersioningStrategy.js +2 -2
  68. package/tsdoc-metadata.json +1 -1
  69. package/turbo/schemas/DryRun.js +8 -14
  70. package/turbo/schemas/results.js +8 -8
  71. package/turbo/services/TurboInspector.js +30 -23
  72. package/utils/ToolCommand.js +38 -12
@@ -1,13 +1,4 @@
1
1
  //#region src/lint/cli/templates/markdownlint.gen.ts
2
- /**
3
- * Markdownlint template data.
4
- *
5
- * Originally generated in the now-deprecated `@savvy-web/lint-staged` package;
6
- * the generator does not live in this repo, so this file is maintained by hand.
7
- * Edit the `MARKDOWNLINT_TEMPLATE` / `MARKDOWNLINT_CONFIG` objects directly.
8
- *
9
- * @internal
10
- */
11
2
  /** Full markdownlint-cli2 config template. */
12
3
  const MARKDOWNLINT_TEMPLATE = {
13
4
  $schema: "https://raw.githubusercontent.com/DavidAnson/markdownlint-cli2/v0.22.0/schema/markdownlint-cli2-config-schema.json",
@@ -1,6 +1,8 @@
1
1
  import { Command } from "../utils/Command.js";
2
+ import { Effect, Result } from "effect";
2
3
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
- import { parse, stringify } from "yaml";
4
+ import { Yaml, YamlStringifyOptions } from "@effected/yaml";
5
+ import { format, resolveConfig } from "prettier";
4
6
 
5
7
  //#region src/lint/handlers/PnpmWorkspace.ts
6
8
  /**
@@ -10,12 +12,21 @@ import { parse, stringify } from "yaml";
10
12
  */
11
13
  /**
12
14
  * Default YAML stringify options for consistent formatting.
15
+ *
16
+ * @remarks
17
+ * `lineWidth: 0` disables line wrapping, matching the v3 `yaml` package
18
+ * behavior. `@effected/yaml` emits block sequences without the extra
19
+ * two-space indentation the v3 `yaml` package used, so the stringified
20
+ * output is normalized through Prettier's YAML printer below — probed
21
+ * byte-identical to the v3 `yaml.stringify(..., { indent: 2, lineWidth: 0,
22
+ * singleQuote: false })` output on this repo's real `pnpm-workspace.yaml`
23
+ * and on hostile synthetic shapes (quoted keys, block scalars, nested
24
+ * sequences of maps).
13
25
  */
14
- const DEFAULT_STRINGIFY_OPTIONS = {
26
+ const DEFAULT_STRINGIFY_OPTIONS = new YamlStringifyOptions({
15
27
  indent: 2,
16
- lineWidth: 0,
17
- singleQuote: false
18
- };
28
+ lineWidth: 0
29
+ });
19
30
  /**
20
31
  * Handler for pnpm-workspace.yaml.
21
32
  *
@@ -116,20 +127,23 @@ var PnpmWorkspace = class PnpmWorkspace {
116
127
  const skipSort = options.skipSort ?? false;
117
128
  const skipFormat = options.skipFormat ?? false;
118
129
  const skipLint = options.skipLint ?? false;
119
- return () => {
130
+ return async () => {
120
131
  const filepath = "pnpm-workspace.yaml";
121
132
  if (!existsSync(filepath)) return [];
122
133
  const content = readFileSync(filepath, "utf-8");
123
- let parsed;
124
- try {
125
- parsed = parse(content);
126
- } catch (error) {
127
- if (!skipLint) throw new Error(`Invalid YAML in ${filepath}: ${error instanceof Error ? error.message : String(error)}`);
134
+ const parseResult = Effect.runSync(Effect.result(Yaml.parse(content)));
135
+ if (Result.isFailure(parseResult)) {
136
+ if (!skipLint) throw new Error(`Invalid YAML in ${filepath}: ${parseResult.failure.message}`);
128
137
  return [];
129
138
  }
139
+ let parsed = parseResult.success;
130
140
  if (!skipSort) parsed = PnpmWorkspace.sortContent(parsed);
131
141
  if (!skipSort || !skipFormat) {
132
- writeFileSync(filepath, stringify(parsed, DEFAULT_STRINGIFY_OPTIONS), "utf-8");
142
+ writeFileSync(filepath, await format(Effect.runSync(Yaml.stringify(parsed, DEFAULT_STRINGIFY_OPTIONS)), {
143
+ ...await resolveConfig(filepath),
144
+ filepath,
145
+ parser: "yaml"
146
+ }), "utf-8");
133
147
  return [];
134
148
  }
135
149
  return [];
@@ -1,18 +1,5 @@
1
1
  //#region src/lint/utils/Filter.ts
2
2
  /**
3
- * Utilities for filtering staged file lists.
4
- *
5
- * @example
6
- * ```typescript
7
- * import { Filter } from '@savvy-web/silk/lint';
8
- *
9
- * const handler = (filenames: readonly string[]) => {
10
- * const filtered = Filter.exclude(filenames, ['dist/', '__fixtures__']);
11
- * return filtered.length > 0 ? `biome check ${Filter.shellEscape(filtered)}` : [];
12
- * };
13
- * ```
14
- */
15
- /**
16
3
  * Static utility class for filtering file lists.
17
4
  */
18
5
  var Filter = class Filter {
@@ -1,14 +1,18 @@
1
1
  import { dirname } from "node:path";
2
- import { findWorkspaceRootSync, getWorkspacePackagesSync } from "workspaces-effect";
2
+ import { findWorkspaceRootSync, getWorkspacePackagesSync } from "@effected/workspaces";
3
+ import { nodeSyncOps } from "@effected/workspaces/node-sync";
3
4
 
4
5
  //#region src/lint/utils/Workspace.ts
5
6
  /**
6
7
  * Workspace-aware discovery utilities.
7
8
  *
8
9
  * @remarks
9
- * Wraps the synchronous APIs from `workspaces-effect` with caching.
10
- * Workspace layout does not change during a lint-staged run, so
11
- * results are cached on first access. Use `resetWorkspaceCache()`
10
+ * Wraps the synchronous APIs from `@effected/workspaces` with caching.
11
+ * The kit's sync entry points never import `node:*` themselves — the
12
+ * Node binding is the kit's own `@effected/workspaces/node-sync` preset
13
+ * (`nodeSyncOps`), which replaced this module's hand-wired ops in kit
14
+ * round 3. Workspace layout does not change during a lint-staged run,
15
+ * so results are cached on first access. Use `resetWorkspaceCache()`
12
16
  * in tests to clear state between runs.
13
17
  */
14
18
  /** Sentinel indicating "not yet resolved". */
@@ -23,7 +27,7 @@ let cachedPaths = UNRESOLVED;
23
27
  */
24
28
  function getWorkspaceRoot() {
25
29
  if (cachedRoot !== UNRESOLVED) return cachedRoot;
26
- cachedRoot = findWorkspaceRootSync() ?? null;
30
+ cachedRoot = findWorkspaceRootSync(nodeSyncOps);
27
31
  return cachedRoot;
28
32
  }
29
33
  /**
@@ -38,7 +42,7 @@ function getWorkspacePackages() {
38
42
  cachedPackages = null;
39
43
  return null;
40
44
  }
41
- cachedPackages = (getWorkspacePackagesSync(root) ?? []).filter((pkg) => pkg.path !== root);
45
+ cachedPackages = getWorkspacePackagesSync(root, nodeSyncOps).filter((pkg) => pkg.path !== root);
42
46
  return cachedPackages;
43
47
  }
44
48
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/silk-effects",
3
- "version": "3.3.1",
3
+ "version": "4.0.1",
4
4
  "private": false,
5
5
  "description": "Shared Effect library for Silk Suite conventions",
6
6
  "homepage": "https://github.com/savvy-web/systems/tree/main/packages/silk-effects",
@@ -23,7 +23,8 @@
23
23
  "exports": {
24
24
  ".": {
25
25
  "types": "./index.d.ts",
26
- "import": "./index.js"
26
+ "import": "./index.js",
27
+ "default": "./index.js"
27
28
  },
28
29
  "./package.json": "./package.json"
29
30
  },
@@ -32,28 +33,28 @@
32
33
  "@changesets/config": "^4.0.0-next.6",
33
34
  "@changesets/get-github-info": "^1.0.0-next.3",
34
35
  "@changesets/get-release-plan": "^5.0.0-next.7",
36
+ "@effected/git": "^0.4.0",
37
+ "@effected/glob": "^0.1.1",
38
+ "@effected/jsonc": "^0.2.0",
39
+ "@effected/package-json": "^0.3.0",
40
+ "@effected/walker": "^0.2.1",
41
+ "@effected/workspaces": "^0.3.1",
42
+ "@effected/yaml": "^0.3.0",
35
43
  "@manypkg/get-packages": "^3.1.0",
36
- "jsonc-effect": "^0.3.1",
37
44
  "mdast-util-heading-range": "^4.0.0",
38
45
  "mdast-util-to-string": "^4.0.0",
39
46
  "prettier": "^3.9.5",
40
47
  "remark-gfm": "^4.0.1",
41
48
  "remark-parse": "^11.0.0",
42
49
  "remark-stringify": "^11.0.0",
43
- "semver-effect": "^0.3.1",
44
50
  "shell-quote": "^1.10.0",
45
51
  "sort-package-json": "^4.0.0",
46
- "tinyglobby": "^0.2.17",
47
52
  "unified": "^11.0.5",
48
53
  "unified-lint-rule": "^3.0.1",
49
54
  "unist-util-visit": "^5.1.0",
50
- "workspaces-effect": "^2.1.0",
51
- "yaml": "^2.9.0",
52
- "yaml-effect": "^0.7.2",
53
55
  "yaml-lint": "^1.7.0"
54
56
  },
55
57
  "peerDependencies": {
56
- "@effect/platform": "^0.96.0",
57
- "effect": "^3.21.0"
58
+ "effect": "4.0.0-beta.98"
58
59
  }
59
60
  }
package/repos/index.js CHANGED
@@ -3,8 +3,8 @@ import { MANIFEST_PATH, REPOS_DIR } from "./constants.js";
3
3
  import { GitSubmoduleError, GitSubmoduleErrorBase, NoteNotFoundError, NoteNotFoundErrorBase, RepoNotFoundError, RepoNotFoundErrorBase, ReposConfigError, ReposConfigErrorBase } from "./errors.js";
4
4
  import { RepoEntry, RepoName, RepoNote, RepoOrientation, ReposManifestFile } from "./schemas/manifest.js";
5
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";
6
+ import { ReposConfigStore, ReposConfigStoreLive } from "./services/config-store.js";
7
+ import { ReposManager, ReposManagerLive, STALE_LOCK_MAX_AGE_MS } from "./services/manager.js";
8
8
 
9
9
  //#region src/repos/index.ts
10
10
  var repos_exports = /* @__PURE__ */ __exportAll({
@@ -26,10 +26,8 @@ var repos_exports = /* @__PURE__ */ __exportAll({
26
26
  ReposConfigError: () => ReposConfigError,
27
27
  ReposConfigErrorBase: () => ReposConfigErrorBase,
28
28
  ReposConfigStore: () => ReposConfigStore,
29
- ReposConfigStoreBase: () => ReposConfigStoreBase,
30
29
  ReposConfigStoreLive: () => ReposConfigStoreLive,
31
30
  ReposManager: () => ReposManager,
32
- ReposManagerBase: () => ReposManagerBase,
33
31
  ReposManagerLive: () => ReposManagerLive,
34
32
  ReposManifestFile: () => ReposManifestFile,
35
33
  ReposNoteResult: () => ReposNoteResult,
@@ -40,4 +38,4 @@ var repos_exports = /* @__PURE__ */ __exportAll({
40
38
  });
41
39
 
42
40
  //#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 };
41
+ export { GitSubmoduleError, GitSubmoduleErrorBase, MANIFEST_PATH, NoteNotFoundError, NoteNotFoundErrorBase, REPOS_DIR, RepoEntry, RepoName, RepoNotFoundError, RepoNotFoundErrorBase, RepoNote, RepoOrientation, RepoStatusEntry, ReposAddResult, ReposConfigError, ReposConfigErrorBase, ReposConfigStore, ReposConfigStoreLive, ReposManager, ReposManagerLive, ReposManifestFile, ReposNoteResult, ReposPinResult, ReposStatusReport, ReposSyncReport, STALE_LOCK_MAX_AGE_MS, repos_exports };
@@ -7,7 +7,7 @@ import { Schema } from "effect";
7
7
  * (`.repos/<name>`) without escaping the `.repos/` directory.
8
8
  * @public
9
9
  */
10
- const RepoName = Schema.String.pipe(Schema.pattern(/^(?!\.{1,2}$)[A-Za-z0-9][A-Za-z0-9._-]*$/), Schema.annotations({ identifier: "RepoName" }));
10
+ const RepoName = Schema.String.check(Schema.isPattern(/^(?!\.{1,2}$)[A-Za-z0-9][A-Za-z0-9._-]*$/)).annotate({ identifier: "RepoName" });
11
11
  /**
12
12
  * An agent-appended note: a discovered answer stamped with the pin it was written against.
13
13
  * @public
@@ -16,7 +16,7 @@ const RepoNote = Schema.Struct({
16
16
  id: Schema.String,
17
17
  date: Schema.String,
18
18
  ref: Schema.String,
19
- note: Schema.String.pipe(Schema.minLength(1))
19
+ note: Schema.String.check(Schema.isMinLength(1))
20
20
  });
21
21
  /**
22
22
  * Curated per-repo orientation: layout, entry points, where to start.
@@ -24,10 +24,7 @@ const RepoNote = Schema.Struct({
24
24
  */
25
25
  const RepoOrientation = Schema.Struct({
26
26
  layout: Schema.optional(Schema.String),
27
- keyPaths: Schema.optional(Schema.Record({
28
- key: Schema.String,
29
- value: Schema.String
30
- })),
27
+ keyPaths: Schema.optional(Schema.Record(Schema.String, Schema.String)),
31
28
  startHere: Schema.optional(Schema.String)
32
29
  });
33
30
  /**
@@ -35,9 +32,9 @@ const RepoOrientation = Schema.Struct({
35
32
  * @public
36
33
  */
37
34
  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)),
35
+ url: Schema.String.check(Schema.isMinLength(1)),
36
+ ref: Schema.String.check(Schema.isMinLength(1)),
37
+ purpose: Schema.String.check(Schema.isMinLength(1)),
41
38
  sparse: Schema.optional(Schema.Array(Schema.String)),
42
39
  orientation: Schema.optional(RepoOrientation),
43
40
  notes: Schema.optional(Schema.Array(RepoNote))
@@ -61,10 +58,7 @@ const isRepoName = Schema.is(RepoName);
61
58
  * The committed .repos/config.json manifest.
62
59
  * @public
63
60
  */
64
- const ReposManifestFile = Schema.Struct({ repos: Schema.Record({
65
- key: Schema.String,
66
- value: RepoEntry
67
- }).pipe(Schema.filter((repos) => {
61
+ const ReposManifestFile = Schema.Struct({ repos: Schema.Record(Schema.String, RepoEntry).check(Schema.makeFilter((repos) => {
68
62
  const badKey = Object.keys(repos).find((key) => !isRepoName(key));
69
63
  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
64
  })) });
@@ -62,7 +62,11 @@ const ReposAddResult = Schema.Struct({
62
62
  */
63
63
  const ReposNoteResult = Schema.Struct({
64
64
  name: Schema.String,
65
- op: Schema.Literal("add", "remove", "promote"),
65
+ op: Schema.Literals([
66
+ "add",
67
+ "remove",
68
+ "promote"
69
+ ]),
66
70
  id: Schema.String,
67
71
  noteCount: Schema.Number
68
72
  });
@@ -1,20 +1,16 @@
1
1
  import { MANIFEST_PATH, REPOS_DIR } from "../constants.js";
2
2
  import { ReposConfigError } from "../errors.js";
3
3
  import { ReposManifestFile } from "../schemas/manifest.js";
4
- import { Context, Effect, Layer, Schema } from "effect";
5
- import { FileSystem, Path } from "@effect/platform";
4
+ import { Context, Effect, FileSystem, Layer, Path, Schema } from "effect";
6
5
 
7
6
  //#region src/repos/services/config-store.ts
8
- const _tag = Context.Tag("@savvy-web/silk-effects/ReposConfigStore");
9
- /** @internal */
10
- const ReposConfigStoreBase = _tag();
11
7
  /**
12
8
  * Reads, validates, and writes the .repos/config.json manifest.
13
9
  * @public
14
10
  */
15
- var ReposConfigStore = class extends ReposConfigStoreBase {};
11
+ var ReposConfigStore = class extends Context.Service()("@savvy-web/silk-effects/ReposConfigStore") {};
16
12
  /**
17
- * Live layer over the platform FileSystem.
13
+ * Live layer over the core FileSystem.
18
14
  * @public
19
15
  */
20
16
  const ReposConfigStoreLive = Layer.effect(ReposConfigStore, Effect.gen(function* () {
@@ -45,7 +41,7 @@ const ReposConfigStoreLive = Layer.effect(ReposConfigStore, Effect.gen(function*
45
41
  kind: "invalid"
46
42
  })
47
43
  });
48
- return yield* Schema.decodeUnknown(ReposManifestFile)(json).pipe(Effect.mapError((cause) => new ReposConfigError({
44
+ return yield* Schema.decodeUnknownEffect(ReposManifestFile)(json).pipe(Effect.mapError((cause) => new ReposConfigError({
49
45
  path: manifestPath(root),
50
46
  reason: String(cause),
51
47
  kind: "invalid"
@@ -58,7 +54,7 @@ const ReposConfigStoreLive = Layer.effect(ReposConfigStore, Effect.gen(function*
58
54
  reason: `mkdir failed: ${String(cause)}`,
59
55
  kind: "invalid"
60
56
  })));
61
- const encoded = yield* Schema.encode(ReposManifestFile)(manifest).pipe(Effect.mapError((cause) => new ReposConfigError({
57
+ const encoded = yield* Schema.encodeEffect(ReposManifestFile)(manifest).pipe(Effect.mapError((cause) => new ReposConfigError({
62
58
  path: manifestPath(root),
63
59
  reason: String(cause),
64
60
  kind: "invalid"
@@ -83,4 +79,4 @@ const ReposConfigStoreLive = Layer.effect(ReposConfigStore, Effect.gen(function*
83
79
  }));
84
80
 
85
81
  //#endregion
86
- export { ReposConfigStore, ReposConfigStoreBase, ReposConfigStoreLive };
82
+ export { ReposConfigStore, ReposConfigStoreLive };
@@ -2,8 +2,8 @@ import { MANIFEST_PATH, REPOS_DIR } from "../constants.js";
2
2
  import { GitSubmoduleError, NoteNotFoundError, RepoNotFoundError, ReposConfigError } from "../errors.js";
3
3
  import { RepoName } from "../schemas/manifest.js";
4
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";
5
+ import { Clock, Context, Effect, FileSystem, Layer, Option, Path, Schema } from "effect";
6
+ import { Git } from "@effected/git";
7
7
  import { createHash } from "node:crypto";
8
8
 
9
9
  //#region src/repos/services/manager.ts
@@ -25,9 +25,6 @@ const STALE_LOCKS = ["index.lock", "shallow.lock"];
25
25
  * @public
26
26
  */
27
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
28
  /**
32
29
  * Drives the vendored `.repos/` submodules over git: reports status
33
30
  * (presence, dirtiness, stale notes), reconciles the working tree with the
@@ -35,54 +32,38 @@ const ReposManagerBase = _tag();
35
32
  * ref (`pin`), and adds, removes, or promotes agent notes (`note`).
36
33
  * @public
37
34
  */
38
- var ReposManager = class extends ReposManagerBase {};
35
+ var ReposManager = class extends Context.Service()("@savvy-web/silk-effects/ReposManager") {};
39
36
  /**
40
37
  * Live implementation of {@link ReposManager}.
41
38
  *
42
39
  * @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`.
40
+ * All git plumbing runs through `@effected/git`'s `Git` service (captured once
41
+ * at layer construction; `Git.layer` requires `ChildProcessSpawner` at the app
42
+ * edge), so the public method effects stay at `R = never`. The kit classifies
43
+ * git's exit-code/stderr taxonomy into typed failures, which are mapped onto
44
+ * this module's {@link GitSubmoduleError} to keep the declared error unions.
46
45
  * @public
47
46
  */
48
47
  const ReposManagerLive = Layer.effect(ReposManager, Effect.gen(function* () {
49
48
  const configStore = yield* ReposConfigStore;
50
49
  const fs = yield* FileSystem.FileSystem;
51
50
  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(" ")}`,
51
+ const git = yield* Git;
52
+ /** Map any typed `@effected/git` failure onto this module's `GitSubmoduleError`. */
53
+ const asSubmoduleError = (command, cwd) => (error) => new GitSubmoduleError({
54
+ command,
68
55
  cwd,
69
- reason: String(cause)
70
- }))));
56
+ reason: error.message
57
+ });
71
58
  const isPresent = (repoPath) => fs.readDirectory(repoPath).pipe(Effect.map((files) => files.length > 0), Effect.orElseSucceed(() => false));
72
59
  const status = (root) => Effect.gen(function* () {
73
60
  const manifest = yield* configStore.read(root);
74
61
  const repos = yield* Effect.forEach(Object.entries(manifest.repos), ([name, entry]) => Effect.gen(function* () {
75
62
  const repoPath = path.join(root, REPOS_DIR, name);
76
63
  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;
64
+ const commit = (yield* git.lsTree(root, "HEAD", { pathspec: [`${".repos"}/${name}`] }).pipe(Effect.mapError(asSubmoduleError(`git ls-tree HEAD -- ${".repos"}/${name}`, root))))[0]?.oid ?? null;
84
65
  let dirty = false;
85
- if (present) dirty = (yield* runGit(repoPath, ["status", "--porcelain"])).length > 0;
66
+ if (present) dirty = (yield* git.status(repoPath).pipe(Effect.mapError(asSubmoduleError("git status --porcelain", repoPath)))).length > 0;
86
67
  const staleNoteIds = (entry.notes ?? []).filter((note) => note.ref !== entry.ref).map((note) => note.id);
87
68
  return {
88
69
  name,
@@ -123,24 +104,15 @@ const ReposManagerLive = Layer.effect(ReposManager, Effect.gen(function* () {
123
104
  }
124
105
  if (clearedAnyLock) clearedLocks.push(name);
125
106
  if (!(yield* isPresent(repoPath))) {
126
- yield* runGit(root, [
127
- "submodule",
128
- "update",
129
- "--init",
130
- "--depth",
131
- "1",
132
- "--",
133
- `${REPOS_DIR}/${name}`
134
- ]);
107
+ yield* git.submoduleUpdate(root, {
108
+ init: true,
109
+ depth: 1,
110
+ paths: [`${REPOS_DIR}/${name}`]
111
+ }).pipe(Effect.mapError(asSubmoduleError(`git submodule update --init --depth 1 -- ${REPOS_DIR}/${name}`, root)));
135
112
  initialized.push(name);
136
113
  } else upToDate.push(name);
137
114
  if (entry.sparse && entry.sparse.length > 0) {
138
- yield* runGit(repoPath, [
139
- "sparse-checkout",
140
- "set",
141
- "--no-cone",
142
- ...entry.sparse
143
- ]);
115
+ yield* git.sparseCheckoutSet(repoPath, entry.sparse, { cone: false }).pipe(Effect.mapError(asSubmoduleError("git sparse-checkout set --no-cone", repoPath)));
144
116
  sparseApplied.push(name);
145
117
  }
146
118
  }
@@ -157,27 +129,22 @@ const ReposManagerLive = Layer.effect(ReposManager, Effect.gen(function* () {
157
129
  return last.endsWith(".git") ? last.slice(0, -4) : last;
158
130
  };
159
131
  /**
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>`.
132
+ * Fetch `ref` shallow into a submodule via the kit's `Git.fetchAny`:
133
+ * tag-form fetch first (tags need the explicit `fetch origin tag <ref>`
134
+ * form), falling back to a plain `fetch origin <ref>` on
135
+ * `UnknownRefError` OR `GitCommandError` (unclassified tag-form stderr
136
+ * keeps the v3 any-failure fallback). `NotARepositoryError` deliberately
137
+ * does NOT fall back — a plain fetch in a non-repo fails identically, so
138
+ * retrying only doubled the latency of a certain failure. When both
139
+ * attempts fail, the plain fetch's error surfaces.
163
140
  */
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
- ])));
141
+ const fetchRef = (sub, ref) => git.fetchAny(sub, {
142
+ ref,
143
+ depth: 1
144
+ }).pipe(Effect.mapError(asSubmoduleError(`git fetch --depth 1 origin ${ref}`, sub)));
178
145
  const add = (root, options) => Effect.gen(function* () {
179
146
  const name = options.name ?? repoSlug(options.url);
180
- yield* Schema.decodeUnknown(RepoName)(name).pipe(Effect.mapError(() => new ReposConfigError({
147
+ yield* Schema.decodeUnknownEffect(RepoName)(name).pipe(Effect.mapError(() => new ReposConfigError({
181
148
  path: MANIFEST_PATH,
182
149
  reason: `invalid repo name "${name}": must be non-empty, contain no "/" or "\\", and not be "." or ".."`,
183
150
  kind: "invalid"
@@ -190,33 +157,15 @@ const ReposManagerLive = Layer.effect(ReposManager, Effect.gen(function* () {
190
157
  }));
191
158
  const repoPath = `${REPOS_DIR}/${name}`;
192
159
  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
- ]);
160
+ yield* git.submoduleAdd(root, {
161
+ url: options.url,
162
+ path: repoPath,
163
+ depth: 1
164
+ }).pipe(Effect.mapError(asSubmoduleError(`git submodule add --depth 1 ${options.url} ${repoPath}`, root)));
165
+ yield* git.configSet(root, `submodule.${repoPath}.shallow`, "true", { file: ".gitmodules" }).pipe(Effect.mapError(asSubmoduleError(`git config -f .gitmodules submodule.${repoPath}.shallow true`, root)));
208
166
  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
- ]);
167
+ yield* git.checkout(subPath, "FETCH_HEAD", { detach: true }).pipe(Effect.mapError(asSubmoduleError("git checkout --detach FETCH_HEAD", subPath)));
168
+ if (options.sparse && options.sparse.length > 0) yield* git.sparseCheckoutSet(subPath, options.sparse, { cone: false }).pipe(Effect.mapError(asSubmoduleError("git sparse-checkout set --no-cone", subPath)));
220
169
  const entry = {
221
170
  url: options.url,
222
171
  ref: options.ref,
@@ -227,12 +176,11 @@ const ReposManagerLive = Layer.effect(ReposManager, Effect.gen(function* () {
227
176
  ...manifest.repos,
228
177
  [name]: entry
229
178
  } });
230
- yield* runGit(root, [
231
- "add",
179
+ yield* git.add(root, [
232
180
  ".gitmodules",
233
181
  MANIFEST_PATH,
234
182
  repoPath
235
- ]);
183
+ ]).pipe(Effect.mapError(asSubmoduleError(`git add .gitmodules ${MANIFEST_PATH} ${repoPath}`, root)));
236
184
  return {
237
185
  name,
238
186
  ref: options.ref,
@@ -245,14 +193,10 @@ const ReposManagerLive = Layer.effect(ReposManager, Effect.gen(function* () {
245
193
  if (!entry) return yield* Effect.fail(new RepoNotFoundError({ name }));
246
194
  const repoPath = `${REPOS_DIR}/${name}`;
247
195
  const subPath = path.join(root, repoPath);
248
- const oldCommit = yield* runGit(subPath, ["rev-parse", "HEAD"]).pipe(Effect.orElseSucceed(() => null));
196
+ const oldCommit = yield* git.revParse(subPath, "HEAD").pipe(Effect.orElseSucceed(() => null));
249
197
  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"]);
198
+ yield* git.checkout(subPath, "FETCH_HEAD", { detach: true }).pipe(Effect.mapError(asSubmoduleError("git checkout --detach FETCH_HEAD", subPath)));
199
+ const newCommit = yield* git.revParse(subPath, "HEAD").pipe(Effect.mapError(asSubmoduleError("git rev-parse HEAD", subPath)));
256
200
  yield* configStore.write(root, { repos: {
257
201
  ...manifest.repos,
258
202
  [name]: {
@@ -260,11 +204,7 @@ const ReposManagerLive = Layer.effect(ReposManager, Effect.gen(function* () {
260
204
  ref
261
205
  }
262
206
  } });
263
- yield* runGit(root, [
264
- "add",
265
- MANIFEST_PATH,
266
- repoPath
267
- ]);
207
+ yield* git.add(root, [MANIFEST_PATH, repoPath]).pipe(Effect.mapError(asSubmoduleError(`git add ${MANIFEST_PATH} ${repoPath}`, root)));
268
208
  const staleNoteIds = (entry.notes ?? []).filter((note) => note.ref !== ref).map((note) => note.id);
269
209
  return {
270
210
  name,
@@ -371,14 +311,14 @@ const ReposManagerLive = Layer.effect(ReposManager, Effect.gen(function* () {
371
311
  noteCount: updatedNotes.length
372
312
  };
373
313
  });
374
- return ReposManager.of({
314
+ return {
375
315
  status,
376
316
  sync,
377
317
  add,
378
318
  pin,
379
319
  note
380
- });
320
+ };
381
321
  }));
382
322
 
383
323
  //#endregion
384
- export { ReposManager, ReposManagerBase, ReposManagerLive, STALE_LOCK_MAX_AGE_MS };
324
+ export { ReposManager, ReposManagerLive, STALE_LOCK_MAX_AGE_MS };
@@ -11,7 +11,7 @@ import { Schema } from "effect";
11
11
  * @since 0.1.0
12
12
  * @public
13
13
  */
14
- const CommentStyle = Schema.Literal("#", "//");
14
+ const CommentStyle = Schema.Literals(["#", "//"]);
15
15
 
16
16
  //#endregion
17
17
  export { CommentStyle };