authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-02 10:45:44-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-02 20:46:05-08:00
logdde48e94141370c38166e20dde0cec3c347164d5
tree5c4795b68a3a104899d088dc9ef87bce56452e17
parentb1608f41adff7ac9670c5a762624e13984d820c8

Move branch sanitization from contracts to new shared package

Create @t3tools/shared with explicit subpath exports (no barrel) for shared utility logic. Move sanitizeBranchFragment and sanitizeFeatureBranchName to @t3tools/shared/git, keeping contracts focused on schemas. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

10 files changed, 75 insertions(+), 43 deletions(-)

apps/server/package.json+1
......@@ -30,6 +30,7 @@
3030 "@effect/language-service": "0.75.1",
3131 "@effect/vitest": "catalog:",
3232 "@t3tools/contracts": "workspace:*",
33 "@t3tools/shared": "workspace:*",
3334 "@t3tools/web": "workspace:*",
3435 "@types/bun": "^1.3.9",
3536 "@types/node": "^24.10.13",
apps/server/src/git/Layers/CodexTextGeneration.ts+1-1
......@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
33import { Effect, FileSystem, Layer, Option, Path, Schema, Stream } from "effect";
44import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
55
6import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/contracts";
6import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";
77
88import { resolveAttachmentPath } from "../../attachmentStore.ts";
99import { ServerConfig } from "../../config.ts";
apps/server/src/git/Layers/GitManager.ts+1-1
......@@ -1,7 +1,7 @@
11import { randomUUID } from "node:crypto";
22
33import { Effect, FileSystem, Layer, Path } from "effect";
4import { sanitizeFeatureBranchName } from "@t3tools/contracts";
4import { sanitizeFeatureBranchName } from "@t3tools/shared/git";
55
66import { GitManagerError } from "../Errors.ts";
77import { GitManager, type GitManagerShape } from "../Services/GitManager.ts";
apps/web/package.json+1
......@@ -15,6 +15,7 @@
1515 "@lexical/react": "^0.41.0",
1616 "@pierre/diffs": "^1.1.0-beta.16",
1717 "@t3tools/contracts": "workspace:*",
18 "@t3tools/shared": "workspace:*",
1819 "@tanstack/react-pacer": "^0.19.4",
1920 "@tanstack/react-query": "^5.90.0",
2021 "@tanstack/react-router": "^1.160.2",
apps/web/src/components/GitActionsControl.logic.ts+5-5
......@@ -1,9 +1,9 @@
1import {
2 type GitRunStackedActionResult,
3 type GitStackedAction,
4 type GitStatusResult,
5 sanitizeBranchFragment,
1import type {
2 GitRunStackedActionResult,
3 GitStackedAction,
4 GitStatusResult,
65} from "@t3tools/contracts";
6import { sanitizeBranchFragment } from "@t3tools/shared/git";
77
88export type GitActionIconName = "commit" | "push" | "pr";
99
bun.lock+11
......@@ -50,6 +50,7 @@
5050 "@effect/language-service": "0.75.1",
5151 "@effect/vitest": "catalog:",
5252 "@t3tools/contracts": "workspace:*",
53 "@t3tools/shared": "workspace:*",
5354 "@t3tools/web": "workspace:*",
5455 "@types/bun": "^1.3.9",
5556 "@types/node": "^24.10.13",
......@@ -67,6 +68,7 @@
6768 "@lexical/react": "^0.41.0",
6869 "@pierre/diffs": "^1.1.0-beta.16",
6970 "@t3tools/contracts": "workspace:*",
71 "@t3tools/shared": "workspace:*",
7072 "@tanstack/react-pacer": "^0.19.4",
7173 "@tanstack/react-query": "^5.90.0",
7274 "@tanstack/react-router": "^1.160.2",
......@@ -110,6 +112,13 @@
110112 "typescript": "^5.7.3",
111113 },
112114 },
115 "packages/shared": {
116 "name": "@t3tools/shared",
117 "version": "0.0.0-alpha.1",
118 "devDependencies": {
119 "typescript": "^5.7.3",
120 },
121 },
113122 },
114123 "catalog": {
115124 "@effect/language-service": "0.75.1",
......@@ -537,6 +546,8 @@
537546
538547 "@t3tools/desktop": ["@t3tools/desktop@workspace:apps/desktop"],
539548
549 "@t3tools/shared": ["@t3tools/shared@workspace:packages/shared"],
550
540551 "@t3tools/web": ["@t3tools/web@workspace:apps/web"],
541552
542553 "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
packages/contracts/src/git.ts-36
......@@ -179,39 +179,3 @@ export const GitPullResult = Schema.Struct({
179179 upstreamBranch: TrimmedNonEmptyStringSchema.pipe(Schema.NullOr),
180180});
181181export type GitPullResult = typeof GitPullResult.Type;
182
183// Branch name utilities
184
185/**
186 * Sanitize an arbitrary string into a valid, lowercase git branch fragment.
187 * Strips quotes, collapses separators, limits to 64 chars.
188 */
189export function sanitizeBranchFragment(raw: string): string {
190 const normalized = raw
191 .trim()
192 .toLowerCase()
193 .replace(/['"`]/g, "")
194 .replace(/^[./\s_-]+|[./\s_-]+$/g, "");
195
196 const branchFragment = normalized
197 .replace(/[^a-z0-9/_-]+/g, "-")
198 .replace(/\/+/g, "/")
199 .replace(/-+/g, "-")
200 .replace(/^[./_-]+|[./_-]+$/g, "")
201 .slice(0, 64)
202 .replace(/[./_-]+$/g, "");
203
204 return branchFragment.length > 0 ? branchFragment : "update";
205}
206
207/**
208 * Sanitize a string into a `feature/…` branch name.
209 * Preserves an existing `feature/` prefix or slash-separated namespace.
210 */
211export function sanitizeFeatureBranchName(raw: string): string {
212 const sanitized = sanitizeBranchFragment(raw);
213 if (sanitized.includes("/")) {
214 return sanitized.startsWith("feature/") ? sanitized : `feature/${sanitized}`;
215 }
216 return `feature/${sanitized}`;
217}
packages/shared/package.json created+18
......@@ -0,0 +1,18 @@
1{
2 "name": "@t3tools/shared",
3 "version": "0.0.0-alpha.1",
4 "private": true,
5 "type": "module",
6 "exports": {
7 "./git": {
8 "types": "./src/git.ts",
9 "import": "./src/git.ts"
10 }
11 },
12 "scripts": {
13 "typecheck": "tsc --noEmit"
14 },
15 "devDependencies": {
16 "typescript": "^5.7.3"
17 }
18}
packages/shared/src/git.ts created+33
......@@ -0,0 +1,33 @@
1/**
2 * Sanitize an arbitrary string into a valid, lowercase git branch fragment.
3 * Strips quotes, collapses separators, limits to 64 chars.
4 */
5export function sanitizeBranchFragment(raw: string): string {
6 const normalized = raw
7 .trim()
8 .toLowerCase()
9 .replace(/['"`]/g, "")
10 .replace(/^[./\s_-]+|[./\s_-]+$/g, "");
11
12 const branchFragment = normalized
13 .replace(/[^a-z0-9/_-]+/g, "-")
14 .replace(/\/+/g, "/")
15 .replace(/-+/g, "-")
16 .replace(/^[./_-]+|[./_-]+$/g, "")
17 .slice(0, 64)
18 .replace(/[./_-]+$/g, "");
19
20 return branchFragment.length > 0 ? branchFragment : "update";
21}
22
23/**
24 * Sanitize a string into a `feature/…` branch name.
25 * Preserves an existing `feature/` prefix or slash-separated namespace.
26 */
27export function sanitizeFeatureBranchName(raw: string): string {
28 const sanitized = sanitizeBranchFragment(raw);
29 if (sanitized.includes("/")) {
30 return sanitized.startsWith("feature/") ? sanitized : `feature/${sanitized}`;
31 }
32 return `feature/${sanitized}`;
33}
packages/shared/tsconfig.json created+4
......@@ -0,0 +1,4 @@
1{
2 "extends": "../../tsconfig.base.json",
3 "include": ["src"]
4}