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 @@...@@ -30,6 +30,7 @@
30 "@effect/language-service": "0.75.1",30 "@effect/language-service": "0.75.1",
31 "@effect/vitest": "catalog:",31 "@effect/vitest": "catalog:",
32 "@t3tools/contracts": "workspace:*",32 "@t3tools/contracts": "workspace:*",
33 "@t3tools/shared": "workspace:*",
33 "@t3tools/web": "workspace:*",34 "@t3tools/web": "workspace:*",
34 "@types/bun": "^1.3.9",35 "@types/bun": "^1.3.9",
35 "@types/node": "^24.10.13",36 "@types/node": "^24.10.13",
apps/server/src/git/Layers/CodexTextGeneration.ts+1-1
...@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";...@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
3import { Effect, FileSystem, Layer, Option, Path, Schema, Stream } from "effect";3import { Effect, FileSystem, Layer, Option, Path, Schema, Stream } from "effect";
4import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";4import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
55
6import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/contracts";6import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";
77
8import { resolveAttachmentPath } from "../../attachmentStore.ts";8import { resolveAttachmentPath } from "../../attachmentStore.ts";
9import { ServerConfig } from "../../config.ts";9import { ServerConfig } from "../../config.ts";
apps/server/src/git/Layers/GitManager.ts+1-1
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import { randomUUID } from "node:crypto";1import { randomUUID } from "node:crypto";
22
3import { Effect, FileSystem, Layer, Path } from "effect";3import { Effect, FileSystem, Layer, Path } from "effect";
4import { sanitizeFeatureBranchName } from "@t3tools/contracts";4import { sanitizeFeatureBranchName } from "@t3tools/shared/git";
55
6import { GitManagerError } from "../Errors.ts";6import { GitManagerError } from "../Errors.ts";
7import { GitManager, type GitManagerShape } from "../Services/GitManager.ts";7import { GitManager, type GitManagerShape } from "../Services/GitManager.ts";
apps/web/package.json+1
...@@ -15,6 +15,7 @@...@@ -15,6 +15,7 @@
15 "@lexical/react": "^0.41.0",15 "@lexical/react": "^0.41.0",
16 "@pierre/diffs": "^1.1.0-beta.16",16 "@pierre/diffs": "^1.1.0-beta.16",
17 "@t3tools/contracts": "workspace:*",17 "@t3tools/contracts": "workspace:*",
18 "@t3tools/shared": "workspace:*",
18 "@tanstack/react-pacer": "^0.19.4",19 "@tanstack/react-pacer": "^0.19.4",
19 "@tanstack/react-query": "^5.90.0",20 "@tanstack/react-query": "^5.90.0",
20 "@tanstack/react-router": "^1.160.2",21 "@tanstack/react-router": "^1.160.2",
apps/web/src/components/GitActionsControl.logic.ts+5-5
...@@ -1,9 +1,9 @@...@@ -1,9 +1,9 @@
1import {1import type {
2 type GitRunStackedActionResult,2 GitRunStackedActionResult,
3 type GitStackedAction,3 GitStackedAction,
4 type GitStatusResult,4 GitStatusResult,
5 sanitizeBranchFragment,
6} from "@t3tools/contracts";5} from "@t3tools/contracts";
6import { sanitizeBranchFragment } from "@t3tools/shared/git";
77
8export type GitActionIconName = "commit" | "push" | "pr";8export type GitActionIconName = "commit" | "push" | "pr";
99
bun.lock+11
...@@ -50,6 +50,7 @@...@@ -50,6 +50,7 @@
50 "@effect/language-service": "0.75.1",50 "@effect/language-service": "0.75.1",
51 "@effect/vitest": "catalog:",51 "@effect/vitest": "catalog:",
52 "@t3tools/contracts": "workspace:*",52 "@t3tools/contracts": "workspace:*",
53 "@t3tools/shared": "workspace:*",
53 "@t3tools/web": "workspace:*",54 "@t3tools/web": "workspace:*",
54 "@types/bun": "^1.3.9",55 "@types/bun": "^1.3.9",
55 "@types/node": "^24.10.13",56 "@types/node": "^24.10.13",
...@@ -67,6 +68,7 @@...@@ -67,6 +68,7 @@
67 "@lexical/react": "^0.41.0",68 "@lexical/react": "^0.41.0",
68 "@pierre/diffs": "^1.1.0-beta.16",69 "@pierre/diffs": "^1.1.0-beta.16",
69 "@t3tools/contracts": "workspace:*",70 "@t3tools/contracts": "workspace:*",
71 "@t3tools/shared": "workspace:*",
70 "@tanstack/react-pacer": "^0.19.4",72 "@tanstack/react-pacer": "^0.19.4",
71 "@tanstack/react-query": "^5.90.0",73 "@tanstack/react-query": "^5.90.0",
72 "@tanstack/react-router": "^1.160.2",74 "@tanstack/react-router": "^1.160.2",
...@@ -110,6 +112,13 @@...@@ -110,6 +112,13 @@
110 "typescript": "^5.7.3",112 "typescript": "^5.7.3",
111 },113 },
112 },114 },
115 "packages/shared": {
116 "name": "@t3tools/shared",
117 "version": "0.0.0-alpha.1",
118 "devDependencies": {
119 "typescript": "^5.7.3",
120 },
121 },
113 },122 },
114 "catalog": {123 "catalog": {
115 "@effect/language-service": "0.75.1",124 "@effect/language-service": "0.75.1",
...@@ -537,6 +546,8 @@...@@ -537,6 +546,8 @@
537546
538 "@t3tools/desktop": ["@t3tools/desktop@workspace:apps/desktop"],547 "@t3tools/desktop": ["@t3tools/desktop@workspace:apps/desktop"],
539548
549 "@t3tools/shared": ["@t3tools/shared@workspace:packages/shared"],
550
540 "@t3tools/web": ["@t3tools/web@workspace:apps/web"],551 "@t3tools/web": ["@t3tools/web@workspace:apps/web"],
541552
542 "@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=="],553 "@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({...@@ -179,39 +179,3 @@ export const GitPullResult = Schema.Struct({
179 upstreamBranch: TrimmedNonEmptyStringSchema.pipe(Schema.NullOr),179 upstreamBranch: TrimmedNonEmptyStringSchema.pipe(Schema.NullOr),
180});180});
181export type GitPullResult = typeof GitPullResult.Type;181export 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}