@hunsu/core 0.1.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/LICENSE +157 -0
- package/dist/artifact-actions.d.ts +83 -0
- package/dist/artifact-actions.js +415 -0
- package/dist/board.d.ts +15 -0
- package/dist/board.js +223 -0
- package/dist/domain-store.d.ts +268 -0
- package/dist/domain-store.js +951 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +34 -0
- package/dist/git.d.ts +105 -0
- package/dist/git.js +323 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/lookup.d.ts +7 -0
- package/dist/lookup.js +57 -0
- package/dist/model.d.ts +71 -0
- package/dist/model.js +18 -0
- package/dist/port.d.ts +56 -0
- package/dist/port.js +186 -0
- package/dist/primitives.d.ts +48 -0
- package/dist/primitives.js +108 -0
- package/dist/trailer.d.ts +7 -0
- package/dist/trailer.js +67 -0
- package/package.json +33 -0
- package/src/artifact-actions.ts +531 -0
- package/src/board.ts +272 -0
- package/src/domain-store.ts +1263 -0
- package/src/errors.ts +39 -0
- package/src/git.ts +445 -0
- package/src/index.ts +10 -0
- package/src/lookup.ts +72 -0
- package/src/model.ts +109 -0
- package/src/port.ts +263 -0
- package/src/primitives.ts +160 -0
- package/src/trailer.ts +79 -0
package/src/port.ts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
|
+
import { type Command, type DestinationSeedInput, type HarnessSnapshot } from "@hunsu/protocol";
|
|
4
|
+
import { hasHunsuRuntimeState, loadDomainStore, readDomainEventRefs, writeCommands } from "./domain-store.ts";
|
|
5
|
+
import { GitError } from "./errors.ts";
|
|
6
|
+
import { ensureGitRepository, git } from "./git.ts";
|
|
7
|
+
|
|
8
|
+
export type HunsuPortInspection = {
|
|
9
|
+
root: string;
|
|
10
|
+
isGitRepository: boolean;
|
|
11
|
+
isHunsuRoadmap: boolean;
|
|
12
|
+
packageManager?: "pnpm" | "npm" | "yarn" | "bun";
|
|
13
|
+
packageScripts: Record<string, string>;
|
|
14
|
+
docker: {
|
|
15
|
+
dockerfiles: string[];
|
|
16
|
+
composeFiles: string[];
|
|
17
|
+
};
|
|
18
|
+
artifactActions: {
|
|
19
|
+
configured: boolean;
|
|
20
|
+
count: number;
|
|
21
|
+
issues: string[];
|
|
22
|
+
};
|
|
23
|
+
env: {
|
|
24
|
+
files: string[];
|
|
25
|
+
hardcodedHints: string[];
|
|
26
|
+
};
|
|
27
|
+
recommendedFiles: HunsuPortFilePlan[];
|
|
28
|
+
issues: string[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type HunsuPortFilePlan = {
|
|
32
|
+
path: string;
|
|
33
|
+
action: "create";
|
|
34
|
+
reason: string;
|
|
35
|
+
content: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type HunsuPortPlanInput = {
|
|
39
|
+
cwd?: string;
|
|
40
|
+
title?: string;
|
|
41
|
+
goal?: string;
|
|
42
|
+
destinations?: DestinationSeedInput[];
|
|
43
|
+
harness?: HarnessSnapshot;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type HunsuPortPlan = {
|
|
47
|
+
root: string;
|
|
48
|
+
title: string;
|
|
49
|
+
goal: string;
|
|
50
|
+
destinations: DestinationSeedInput[];
|
|
51
|
+
inspection: HunsuPortInspection;
|
|
52
|
+
files: HunsuPortFilePlan[];
|
|
53
|
+
commands: Command[];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type HunsuPortApplyResult = {
|
|
57
|
+
root: string;
|
|
58
|
+
plan: HunsuPortPlan;
|
|
59
|
+
writtenFiles: string[];
|
|
60
|
+
acceptedEvents: ReturnType<typeof writeCommands>["acceptedEvents"];
|
|
61
|
+
board: ReturnType<typeof writeCommands>["board"];
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type PackageJson = {
|
|
65
|
+
scripts?: Record<string, string>;
|
|
66
|
+
packageManager?: string;
|
|
67
|
+
dependencies?: Record<string, string>;
|
|
68
|
+
devDependencies?: Record<string, string>;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const DEFAULT_PORT_TITLE = "Port Git Project";
|
|
72
|
+
const DEFAULT_PORT_GOAL = "Make this project ready for Hunsu-managed MOVEs and Artifact Actions.";
|
|
73
|
+
|
|
74
|
+
export function inspectHunsuPort(cwd = process.cwd()): HunsuPortInspection {
|
|
75
|
+
const target = resolve(cwd);
|
|
76
|
+
const issues: string[] = [];
|
|
77
|
+
let root = target;
|
|
78
|
+
let isGitRepository = false;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
root = ensureGitRepository(target);
|
|
82
|
+
isGitRepository = true;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
issues.push("Not a Git repository. Hunsu Port expects an existing Git project.");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const packageJson = readPackageJson(root);
|
|
88
|
+
const packageScripts = packageJson?.scripts ?? {};
|
|
89
|
+
const packageManager = detectPackageManager(root, packageJson);
|
|
90
|
+
const dockerfiles = listRootFiles(root, /^Dockerfile(?:\.|$)/);
|
|
91
|
+
const composeFiles = listRootFiles(root, /^(?:docker-)?compose(?:\.[^.]+)?\.ya?ml$/);
|
|
92
|
+
const isHunsuRoadmap = isGitRepository && hasHunsuStateOrLegacyRefs(root);
|
|
93
|
+
const artifactActions = inspectArtifactActions(root);
|
|
94
|
+
const env = inspectEnvHints(root, packageScripts);
|
|
95
|
+
const recommendedFiles: HunsuPortFilePlan[] = [];
|
|
96
|
+
|
|
97
|
+
if (!packageJson && isGitRepository) {
|
|
98
|
+
issues.push("No package.json found. Port can still create Roadmap state, but Artifact Actions may need a later Hunsu setup.");
|
|
99
|
+
}
|
|
100
|
+
if (isGitRepository && !isHunsuRoadmap) {
|
|
101
|
+
issues.push("Hunsu runtime state is missing. Port apply will create Initial Team state.");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
root,
|
|
106
|
+
isGitRepository,
|
|
107
|
+
isHunsuRoadmap,
|
|
108
|
+
packageManager,
|
|
109
|
+
packageScripts,
|
|
110
|
+
docker: {
|
|
111
|
+
dockerfiles,
|
|
112
|
+
composeFiles
|
|
113
|
+
},
|
|
114
|
+
artifactActions,
|
|
115
|
+
env,
|
|
116
|
+
recommendedFiles,
|
|
117
|
+
issues
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function planHunsuPort(input: HunsuPortPlanInput = {}): HunsuPortPlan {
|
|
122
|
+
const inspection = inspectHunsuPort(input.cwd);
|
|
123
|
+
if (!inspection.isGitRepository) {
|
|
124
|
+
throw new Error("Hunsu Port requires an existing Git repository");
|
|
125
|
+
}
|
|
126
|
+
const title = input.title?.trim() || basename(inspection.root) || DEFAULT_PORT_TITLE;
|
|
127
|
+
const goal = input.goal?.trim() || DEFAULT_PORT_GOAL;
|
|
128
|
+
const destinations = input.destinations && input.destinations.length > 0
|
|
129
|
+
? input.destinations
|
|
130
|
+
: [{ id: "destination_001", title: "Establish artifact-action-ready product execution", priority: 100 }];
|
|
131
|
+
const commands: Command[] = inspection.isHunsuRoadmap
|
|
132
|
+
? []
|
|
133
|
+
: [{
|
|
134
|
+
type: "CreateInitialTeam",
|
|
135
|
+
requestId: createId("req", title),
|
|
136
|
+
lineId: `run/${createId("req", title)}`,
|
|
137
|
+
title,
|
|
138
|
+
goal,
|
|
139
|
+
destinations,
|
|
140
|
+
harness: input.harness
|
|
141
|
+
}];
|
|
142
|
+
return {
|
|
143
|
+
root: inspection.root,
|
|
144
|
+
title,
|
|
145
|
+
goal,
|
|
146
|
+
destinations,
|
|
147
|
+
inspection,
|
|
148
|
+
files: [],
|
|
149
|
+
commands
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function applyHunsuPort(input: HunsuPortPlanInput = {}): HunsuPortApplyResult {
|
|
154
|
+
const plan = planHunsuPort(input);
|
|
155
|
+
const writtenFiles: string[] = [];
|
|
156
|
+
for (const file of plan.files) {
|
|
157
|
+
const path = join(plan.root, file.path);
|
|
158
|
+
if (existsSync(path)) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
writeFileSync(path, file.content, "utf8");
|
|
162
|
+
writtenFiles.push(file.path);
|
|
163
|
+
}
|
|
164
|
+
const result = plan.commands.length > 0
|
|
165
|
+
? writeCommands(plan.commands, { cwd: plan.root, commitMessage: `hunsu: port ${plan.title}` })
|
|
166
|
+
: { acceptedEvents: [], board: loadDomainStore(plan.root).board };
|
|
167
|
+
return {
|
|
168
|
+
root: plan.root,
|
|
169
|
+
plan,
|
|
170
|
+
writtenFiles,
|
|
171
|
+
acceptedEvents: result.acceptedEvents,
|
|
172
|
+
board: result.board
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function inspectArtifactActions(root: string): HunsuPortInspection["artifactActions"] {
|
|
177
|
+
if (!hasHunsuStateOrLegacyRefs(root)) {
|
|
178
|
+
return {
|
|
179
|
+
configured: false,
|
|
180
|
+
count: 0,
|
|
181
|
+
issues: []
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const board = loadDomainStore(root).board;
|
|
185
|
+
return {
|
|
186
|
+
configured: board.artifactActions.length > 0,
|
|
187
|
+
count: board.artifactActions.length,
|
|
188
|
+
issues: []
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function detectPackageManager(root: string, packageJson: PackageJson | undefined): HunsuPortInspection["packageManager"] {
|
|
193
|
+
if (existsSync(join(root, "pnpm-lock.yaml")) || packageJson?.packageManager?.startsWith("pnpm@")) {
|
|
194
|
+
return "pnpm";
|
|
195
|
+
}
|
|
196
|
+
if (existsSync(join(root, "yarn.lock")) || packageJson?.packageManager?.startsWith("yarn@")) {
|
|
197
|
+
return "yarn";
|
|
198
|
+
}
|
|
199
|
+
if (existsSync(join(root, "bun.lockb")) || existsSync(join(root, "bun.lock")) || packageJson?.packageManager?.startsWith("bun@")) {
|
|
200
|
+
return "bun";
|
|
201
|
+
}
|
|
202
|
+
if (existsSync(join(root, "package-lock.json")) || packageJson) {
|
|
203
|
+
return "npm";
|
|
204
|
+
}
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function readPackageJson(root: string): PackageJson | undefined {
|
|
209
|
+
const path = join(root, "package.json");
|
|
210
|
+
if (!existsSync(path)) {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
return JSON.parse(readFileSync(path, "utf8")) as PackageJson;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function listRootFiles(root: string, pattern: RegExp): string[] {
|
|
217
|
+
if (!existsSync(root) || !statSync(root).isDirectory()) {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
return readdirSync(root)
|
|
221
|
+
.filter(file => pattern.test(file))
|
|
222
|
+
.sort();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function hasHunsuStateOrLegacyRefs(root: string): boolean {
|
|
226
|
+
try {
|
|
227
|
+
return hasHunsuRuntimeState(root)
|
|
228
|
+
|| readDomainEventRefs(root).length > 0
|
|
229
|
+
|| git(["for-each-ref", "--count=1", "--format=%(refname)", "refs/hunsu"], { cwd: root }).trim().length > 0;
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (error instanceof GitError) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function inspectEnvHints(root: string, scripts: Record<string, string>): HunsuPortInspection["env"] {
|
|
239
|
+
const files = [".env", ".env.local", ".env.development", ".env.example"].filter(file => existsSync(join(root, file)));
|
|
240
|
+
const hardcodedHints = Object.entries(scripts)
|
|
241
|
+
.filter(([, script]) => /\b(?:3000|5173|8080|4187)\b|localhost|127\.0\.0\.1/.test(script))
|
|
242
|
+
.map(([name]) => `package.json scripts.${name}`);
|
|
243
|
+
for (const file of ["vite.config.ts", "vite.config.js", "next.config.js", "next.config.mjs"]) {
|
|
244
|
+
const path = join(root, file);
|
|
245
|
+
if (!existsSync(path)) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const text = readFileSync(path, "utf8");
|
|
249
|
+
if (/\b(?:3000|5173|8080|4187)\b|localhost|127\.0\.0\.1/.test(text)) {
|
|
250
|
+
hardcodedHints.push(file);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return { files, hardcodedHints };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function createId(prefix: string, value: string): string {
|
|
257
|
+
const slug = value
|
|
258
|
+
.toLowerCase()
|
|
259
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
260
|
+
.replace(/^-|-$/g, "")
|
|
261
|
+
.slice(0, 48);
|
|
262
|
+
return `${prefix}_${slug || "project"}`;
|
|
263
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { err, ok, type Result } from "@hunsu/protocol";
|
|
2
|
+
import { HunsuError } from "./errors.ts";
|
|
3
|
+
|
|
4
|
+
export type CoreBrand<T, Name extends string> = T & { readonly __coreBrand: Name };
|
|
5
|
+
|
|
6
|
+
export type CommitSha = CoreBrand<string, "CommitSha">;
|
|
7
|
+
export type GitRef = CoreBrand<string, "GitRef">;
|
|
8
|
+
export type BranchName = CoreBrand<string, "BranchName">;
|
|
9
|
+
export type RuntimeChecksum = CoreBrand<string, "RuntimeChecksum">;
|
|
10
|
+
export type RuntimeSchemaName = CoreBrand<string, "RuntimeSchemaName">;
|
|
11
|
+
export type GitRunId = CoreBrand<string, "GitRunId">;
|
|
12
|
+
export type GitMoveNumber = CoreBrand<string, "GitMoveNumber">;
|
|
13
|
+
export type GitMoveId = CoreBrand<string, "GitMoveId">;
|
|
14
|
+
export type GitGoal = CoreBrand<string, "GitGoal">;
|
|
15
|
+
export type GitPriority = CoreBrand<string, "GitPriority">;
|
|
16
|
+
export type GitTrailerRole = CoreBrand<string, "GitTrailerRole">;
|
|
17
|
+
export type GitTrailerActor = CoreBrand<string, "GitTrailerActor">;
|
|
18
|
+
export type GitHunsuId = CoreBrand<string, "GitHunsuId">;
|
|
19
|
+
|
|
20
|
+
export type CorePrimitiveError = {
|
|
21
|
+
type: "CorePrimitiveError";
|
|
22
|
+
field: string;
|
|
23
|
+
message: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function makeCommitSha(value: unknown, field = "commitSha"): Result<CommitSha, CorePrimitiveError> {
|
|
27
|
+
return typeof value === "string" && /^[0-9a-f]{40}$/i.test(value)
|
|
28
|
+
? ok(value as CommitSha)
|
|
29
|
+
: corePrimitiveError(field, "must be a 40-character Git commit SHA");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function makeGitRef(value: unknown, field = "gitRef"): Result<GitRef, CorePrimitiveError> {
|
|
33
|
+
return makeGitToken(value, field, "must be a non-empty Git ref") as Result<GitRef, CorePrimitiveError>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function makeBranchName(value: unknown, field = "branchName"): Result<BranchName, CorePrimitiveError> {
|
|
37
|
+
return makeGitToken(value, field, "must be a non-empty branch name") as Result<BranchName, CorePrimitiveError>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function makeRuntimeChecksum(value: unknown, field = "runtimeChecksum"): Result<RuntimeChecksum, CorePrimitiveError> {
|
|
41
|
+
return typeof value === "string" && /^[0-9a-f]{64}$/i.test(value)
|
|
42
|
+
? ok(value as RuntimeChecksum)
|
|
43
|
+
: corePrimitiveError(field, "must be a SHA-256 hex checksum");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function makeRuntimeSchemaName(value: unknown, field = "runtimeSchemaName"): Result<RuntimeSchemaName, CorePrimitiveError> {
|
|
47
|
+
return makeNonEmptyCoreString(value, field, "must be a non-empty runtime schema name") as Result<RuntimeSchemaName, CorePrimitiveError>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function makeGitRunId(value: unknown, field = "runId"): Result<GitRunId, CorePrimitiveError> {
|
|
51
|
+
return makeNonEmptyCoreString(value, field, "must be a non-empty Hunsu run id") as Result<GitRunId, CorePrimitiveError>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function makeGitMoveNumber(value: unknown, field = "moveNumber"): Result<GitMoveNumber, CorePrimitiveError> {
|
|
55
|
+
const raw = String(value).trim();
|
|
56
|
+
const digits = /^M\d+$/i.test(raw) ? raw.slice(1) : raw;
|
|
57
|
+
return /^\d+$/.test(digits)
|
|
58
|
+
? ok(digits.padStart(4, "0") as GitMoveNumber)
|
|
59
|
+
: corePrimitiveError(field, "must be numeric");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function makeGitMoveId(value: unknown, field = "moveId"): Result<GitMoveId, CorePrimitiveError> {
|
|
63
|
+
const moveNumber = makeGitMoveNumber(value, field);
|
|
64
|
+
return moveNumber.ok ? ok(`M${moveNumber.value}` as GitMoveId) : moveNumber;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function makeGitGoal(value: unknown, field = "goal"): Result<GitGoal, CorePrimitiveError> {
|
|
68
|
+
return makeGitToken(value, field, "must be a non-empty Hunsu goal") as Result<GitGoal, CorePrimitiveError>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function makeGitPriority(value: unknown, field = "priority"): Result<GitPriority, CorePrimitiveError> {
|
|
72
|
+
return makeGitToken(value, field, "must be a non-empty Hunsu priority") as Result<GitPriority, CorePrimitiveError>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function makeGitTrailerRole(value: unknown, field = "role"): Result<GitTrailerRole, CorePrimitiveError> {
|
|
76
|
+
return makeNonEmptyCoreString(value, field, "must be a non-empty Hunsu role") as Result<GitTrailerRole, CorePrimitiveError>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function makeGitTrailerActor(value: unknown, field = "actor"): Result<GitTrailerActor, CorePrimitiveError> {
|
|
80
|
+
return makeNonEmptyCoreString(value, field, "must be a non-empty Hunsu actor") as Result<GitTrailerActor, CorePrimitiveError>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function makeGitHunsuId(value: unknown, field = "hunsuId"): Result<GitHunsuId, CorePrimitiveError> {
|
|
84
|
+
return makeNonEmptyCoreString(value, field, "must be a non-empty HUNSU id") as Result<GitHunsuId, CorePrimitiveError>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function parseCommitSha(value: string, field = "commitSha"): CommitSha {
|
|
88
|
+
return unwrapCorePrimitive(makeCommitSha(value, field));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function parseGitRef(value: string, field = "gitRef"): GitRef {
|
|
92
|
+
return unwrapCorePrimitive(makeGitRef(value, field));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function parseBranchName(value: string, field = "branchName"): BranchName {
|
|
96
|
+
return unwrapCorePrimitive(makeBranchName(value, field));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function parseRuntimeChecksum(value: string, field = "runtimeChecksum"): RuntimeChecksum {
|
|
100
|
+
return unwrapCorePrimitive(makeRuntimeChecksum(value, field));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function parseRuntimeSchemaName(value: string, field = "runtimeSchemaName"): RuntimeSchemaName {
|
|
104
|
+
return unwrapCorePrimitive(makeRuntimeSchemaName(value, field));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function parseGitRunId(value: string, field = "runId"): GitRunId {
|
|
108
|
+
return unwrapCorePrimitive(makeGitRunId(value, field));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function parseGitMoveNumber(value: string | number, field = "moveNumber"): GitMoveNumber {
|
|
112
|
+
return unwrapCorePrimitive(makeGitMoveNumber(value, field));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function parseGitMoveId(value: string | number, field = "moveId"): GitMoveId {
|
|
116
|
+
return unwrapCorePrimitive(makeGitMoveId(value, field));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function parseGitGoal(value: string, field = "goal"): GitGoal {
|
|
120
|
+
return unwrapCorePrimitive(makeGitGoal(value, field));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function parseGitPriority(value: string, field = "priority"): GitPriority {
|
|
124
|
+
return unwrapCorePrimitive(makeGitPriority(value, field));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function parseGitTrailerRole(value: string, field = "role"): GitTrailerRole {
|
|
128
|
+
return unwrapCorePrimitive(makeGitTrailerRole(value, field));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function parseGitTrailerActor(value: string, field = "actor"): GitTrailerActor {
|
|
132
|
+
return unwrapCorePrimitive(makeGitTrailerActor(value, field));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function parseGitHunsuId(value: string, field = "hunsuId"): GitHunsuId {
|
|
136
|
+
return unwrapCorePrimitive(makeGitHunsuId(value, field));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function unwrapCorePrimitive<T>(result: Result<T, CorePrimitiveError>): T {
|
|
140
|
+
if (result.ok) {
|
|
141
|
+
return result.value;
|
|
142
|
+
}
|
|
143
|
+
throw new HunsuError(`${result.error.field} ${result.error.message}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function makeGitToken<T extends string>(value: unknown, field: string, message: string): Result<CoreBrand<T, string>, CorePrimitiveError> {
|
|
147
|
+
return typeof value === "string" && value.trim().length > 0 && !/[\0\r\n]/.test(value)
|
|
148
|
+
? ok(value as CoreBrand<T, string>)
|
|
149
|
+
: corePrimitiveError(field, message);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function makeNonEmptyCoreString<T extends string>(value: unknown, field: string, message: string): Result<CoreBrand<T, string>, CorePrimitiveError> {
|
|
153
|
+
return typeof value === "string" && value.trim().length > 0
|
|
154
|
+
? ok(value as CoreBrand<T, string>)
|
|
155
|
+
: corePrimitiveError(field, message);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function corePrimitiveError(field: string, message: string): Result<never, CorePrimitiveError> {
|
|
159
|
+
return err({ type: "CorePrimitiveError", field, message });
|
|
160
|
+
}
|
package/src/trailer.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { InvalidTrailerError, MissingTrailerError } from "./errors.ts";
|
|
2
|
+
import type { TrailerMap } from "./model.ts";
|
|
3
|
+
|
|
4
|
+
const TRAILER_LINE = /^([A-Za-z][A-Za-z0-9-]*):[ \t]*(.*)$/;
|
|
5
|
+
|
|
6
|
+
export function parseTrailers(message: string): TrailerMap {
|
|
7
|
+
const lines = message.replace(/\r\n/g, "\n").split("\n");
|
|
8
|
+
let end = lines.length - 1;
|
|
9
|
+
|
|
10
|
+
while (end >= 0 && lines[end].trim() === "") {
|
|
11
|
+
end -= 1;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const trailerLines: string[] = [];
|
|
15
|
+
for (let index = end; index >= 0; index -= 1) {
|
|
16
|
+
const line = lines[index];
|
|
17
|
+
if (line.trim() === "") {
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
if (!TRAILER_LINE.test(line)) {
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
trailerLines.unshift(line);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const trailers: TrailerMap = new Map();
|
|
27
|
+
for (const line of trailerLines) {
|
|
28
|
+
const match = line.match(TRAILER_LINE);
|
|
29
|
+
if (!match) {
|
|
30
|
+
throw new InvalidTrailerError(`Invalid trailer line: ${line}`);
|
|
31
|
+
}
|
|
32
|
+
const [, key, value] = match;
|
|
33
|
+
const values = trailers.get(key) ?? [];
|
|
34
|
+
values.push(value.trim());
|
|
35
|
+
trailers.set(key, values);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return trailers;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getTrailer(trailers: TrailerMap, key: string): string | undefined {
|
|
42
|
+
const values = trailers.get(key);
|
|
43
|
+
return values?.[values.length - 1];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function requireTrailer(trailers: TrailerMap, key: string, commitSha: string): string {
|
|
47
|
+
const value = getTrailer(trailers, key);
|
|
48
|
+
if (!value) {
|
|
49
|
+
throw new MissingTrailerError(commitSha, key);
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function serializeTrailers(trailers: Record<string, string | number | boolean | undefined>): string {
|
|
55
|
+
const lines: string[] = [];
|
|
56
|
+
for (const [key, value] of Object.entries(trailers)) {
|
|
57
|
+
if (value === undefined) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
lines.push(`${key}: ${String(value)}`);
|
|
61
|
+
}
|
|
62
|
+
return lines.join("\n");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function formatMoveNumber(value: string | number): string {
|
|
66
|
+
const raw = String(value).trim();
|
|
67
|
+
if (/^M\d+$/i.test(raw)) {
|
|
68
|
+
return raw.slice(1).padStart(4, "0");
|
|
69
|
+
}
|
|
70
|
+
if (/^\d+$/.test(raw)) {
|
|
71
|
+
return raw.padStart(4, "0");
|
|
72
|
+
}
|
|
73
|
+
return raw;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function formatMoveId(value: string | number): string {
|
|
77
|
+
const number = formatMoveNumber(value);
|
|
78
|
+
return /^M/i.test(number) ? number.toUpperCase() : `M${number}`;
|
|
79
|
+
}
|