@zq-silk/yui 0.0.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.
Files changed (95) hide show
  1. package/ARCHITECTURE.md +141 -0
  2. package/LICENSE +21 -0
  3. package/README.md +211 -0
  4. package/dist/agent/adapterCatalog.js +10 -0
  5. package/dist/agent/agent.js +89 -0
  6. package/dist/agent/agentRegistry.js +10 -0
  7. package/dist/agent/argumentPolicy.js +80 -0
  8. package/dist/brief/taskBrief.js +37 -0
  9. package/dist/cli/commandCatalog.js +647 -0
  10. package/dist/cli/completion.js +111 -0
  11. package/dist/cli/completionWizard.js +143 -0
  12. package/dist/cli/dynamicCompletion.js +48 -0
  13. package/dist/cli/helpRenderer.js +32 -0
  14. package/dist/cli/interactionCandidates.js +139 -0
  15. package/dist/cli/interactionPolicy.js +389 -0
  16. package/dist/cli/interactiveSelection.js +185 -0
  17. package/dist/cli/invocationRouter.js +51 -0
  18. package/dist/cli/roleOptionCatalog.js +67 -0
  19. package/dist/cli/roleWizard.js +546 -0
  20. package/dist/cli/selectionPorts.js +1 -0
  21. package/dist/cli/updateCommand.js +22 -0
  22. package/dist/cli.js +402 -0
  23. package/dist/commands/agentCommands.js +196 -0
  24. package/dist/commands/globalRoleCommands.js +367 -0
  25. package/dist/commands/jobCommands.js +100 -0
  26. package/dist/commands/operatorCommands.js +38 -0
  27. package/dist/commands/repositoryCommands.js +86 -0
  28. package/dist/commands/roleConfiguration.js +201 -0
  29. package/dist/commands/taskCommands.js +1344 -0
  30. package/dist/commands/taskContextCommand.js +215 -0
  31. package/dist/commands/taskInputCommands.js +423 -0
  32. package/dist/commands/taskRoleRuntimeStatus.js +152 -0
  33. package/dist/completion/completionInstaller.js +168 -0
  34. package/dist/completion/completionPort.js +1 -0
  35. package/dist/completion/completionState.js +137 -0
  36. package/dist/completion/completionWizard.js +125 -0
  37. package/dist/completion/fileCompletionManager.js +51 -0
  38. package/dist/config/yuiConfig.js +17 -0
  39. package/dist/context/dispatchContext.js +74 -0
  40. package/dist/controller/clientRuntime.js +215 -0
  41. package/dist/controller/controller.js +158 -0
  42. package/dist/controller/controllerMain.js +37 -0
  43. package/dist/controller/fileSchedulerStoreAdapter.js +322 -0
  44. package/dist/controller/runtime.js +31 -0
  45. package/dist/controller/sessionNotify.js +136 -0
  46. package/dist/core/controllerClient.js +127 -0
  47. package/dist/core/controllerServer.js +269 -0
  48. package/dist/core/protocol.js +169 -0
  49. package/dist/decision/decision.js +42 -0
  50. package/dist/doctor/doctor.js +229 -0
  51. package/dist/errors/cliError.js +38 -0
  52. package/dist/event/taskEvent.js +44 -0
  53. package/dist/executor/agentAdapter.js +338 -0
  54. package/dist/executor/agentExecutor.js +144 -0
  55. package/dist/executor/executorRegistry.js +101 -0
  56. package/dist/executor/fileRoleLaunchPlanner.js +156 -0
  57. package/dist/executor/launchPlan.js +16 -0
  58. package/dist/input/inputRequest.js +326 -0
  59. package/dist/message/message.js +69 -0
  60. package/dist/milestone/milestone.js +27 -0
  61. package/dist/operator/operatorContext.js +66 -0
  62. package/dist/output/rolePresentation.js +82 -0
  63. package/dist/output/table.js +77 -0
  64. package/dist/output/terminal.js +198 -0
  65. package/dist/repository/gitWorkspace.js +210 -0
  66. package/dist/repository/repository.js +55 -0
  67. package/dist/repository/taskWorkspacePreparer.js +256 -0
  68. package/dist/role/role.js +246 -0
  69. package/dist/role/systemRoles.js +20 -0
  70. package/dist/run/agentRun.js +102 -0
  71. package/dist/scheduler/activeRoleRunDelivery.js +94 -0
  72. package/dist/scheduler/archivedTaskRuntime.js +12 -0
  73. package/dist/scheduler/leaderFailure.js +18 -0
  74. package/dist/scheduler/leaderWakeupProcessor.js +143 -0
  75. package/dist/scheduler/operatorInputNotificationProcessor.js +85 -0
  76. package/dist/scheduler/operatorNotification.js +17 -0
  77. package/dist/scheduler/pendingWakeup.js +33 -0
  78. package/dist/scheduler/ports.js +1 -0
  79. package/dist/scheduler/roleRunLiveness.js +41 -0
  80. package/dist/scheduler/wakeupQueue.js +13 -0
  81. package/dist/setup/setupCommand.js +317 -0
  82. package/dist/storage/durableFile.js +38 -0
  83. package/dist/storage/storageSchema.js +259 -0
  84. package/dist/storage/taskStore.js +1032 -0
  85. package/dist/task/task.js +216 -0
  86. package/dist/tmux/commandExecutor.js +69 -0
  87. package/dist/tmux/terminalHandoff.js +17 -0
  88. package/dist/tmux/tmuxManager.js +408 -0
  89. package/dist/workItem/workItem.js +45 -0
  90. package/dist/worktree/roleWorkspace.js +62 -0
  91. package/i18n/README.zh-CN.md +205 -0
  92. package/package.json +47 -0
  93. package/skills/yui-leader/SKILL.md +72 -0
  94. package/skills/yui-operator/SKILL.md +57 -0
  95. package/skills/yui-worker/SKILL.md +31 -0
@@ -0,0 +1,269 @@
1
+ import { randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { createConnection, createServer } from "node:net";
4
+ import { dirname, join } from "node:path";
5
+ import { CONTROLLER_DISCOVERY_PATH, CONTROLLER_SOCKET_PATH, MAX_CONTROLLER_MESSAGE_BYTES, ControllerProtocolError, controllerFailure, encodeControllerResponse, isEmptyParams, parseControllerRequest } from "./protocol.js";
6
+ export async function startControllerServer(home, dispatcher) {
7
+ const discoveryPath = join(home, CONTROLLER_DISCOVERY_PATH);
8
+ const socketPath = join(home, CONTROLLER_SOCKET_PATH);
9
+ const runtimeDirectory = dirname(discoveryPath);
10
+ await mkdir(runtimeDirectory, { recursive: true, mode: 0o700 });
11
+ await chmod(runtimeDirectory, 0o700);
12
+ const token = randomBytes(32).toString("hex");
13
+ let closeRunning = async () => undefined;
14
+ const netServer = createServer((socket) => {
15
+ receiveRequest(socket, token, dispatcher, () => closeRunning());
16
+ });
17
+ try {
18
+ await listen(netServer, socketPath);
19
+ }
20
+ catch (error) {
21
+ if (!isAddressInUse(error))
22
+ throw error;
23
+ if (await socketIsReachable(socketPath))
24
+ throw controllerAlreadyRunning();
25
+ await rm(socketPath, { force: true });
26
+ try {
27
+ await listen(netServer, socketPath);
28
+ }
29
+ catch (retryError) {
30
+ if (isAddressInUse(retryError))
31
+ throw controllerAlreadyRunning();
32
+ throw retryError;
33
+ }
34
+ }
35
+ try {
36
+ await chmod(socketPath, 0o600);
37
+ const discovery = Object.freeze({
38
+ pid: process.pid,
39
+ socketPath,
40
+ token
41
+ });
42
+ await writeFile(discoveryPath, `${JSON.stringify(discovery)}\n`, { mode: 0o600 });
43
+ await chmod(discoveryPath, 0o600);
44
+ let resolveClosed = () => undefined;
45
+ const closed = new Promise((resolve) => {
46
+ resolveClosed = resolve;
47
+ });
48
+ let closePromise;
49
+ closeRunning = () => {
50
+ if (closePromise !== undefined)
51
+ return closePromise;
52
+ closePromise = (async () => {
53
+ await closeNetServer(netServer);
54
+ await removeOwnedDiscovery(discoveryPath, token);
55
+ resolveClosed();
56
+ })();
57
+ return closePromise;
58
+ };
59
+ return Object.freeze({ discovery, closed, close: closeRunning });
60
+ }
61
+ catch (error) {
62
+ await closeNetServer(netServer);
63
+ throw error;
64
+ }
65
+ }
66
+ function receiveRequest(socket, token, dispatcher, stop) {
67
+ let buffer = Buffer.alloc(0);
68
+ let complete = false;
69
+ const fail = (code, message, id = "invalid") => {
70
+ if (complete)
71
+ return;
72
+ complete = true;
73
+ sendResponse(socket, controllerFailure(id, code, message));
74
+ };
75
+ socket.on("data", (chunk) => {
76
+ if (complete)
77
+ return;
78
+ buffer = Buffer.concat([buffer, chunk]);
79
+ const newline = buffer.indexOf(0x0a);
80
+ if (newline < 0) {
81
+ if (buffer.length > MAX_CONTROLLER_MESSAGE_BYTES) {
82
+ fail("MESSAGE_TOO_LARGE", "Controller message exceeds 1 MiB.");
83
+ }
84
+ return;
85
+ }
86
+ if (newline > MAX_CONTROLLER_MESSAGE_BYTES) {
87
+ fail("MESSAGE_TOO_LARGE", "Controller message exceeds 1 MiB.");
88
+ return;
89
+ }
90
+ if (buffer.length !== newline + 1) {
91
+ fail("INVALID_REQUEST", "Invalid controller request.");
92
+ return;
93
+ }
94
+ complete = true;
95
+ void routeRequest(socket, buffer.subarray(0, newline).toString("utf8"), token, dispatcher, stop);
96
+ });
97
+ socket.on("end", () => {
98
+ if (!complete)
99
+ fail("INVALID_REQUEST", "Invalid controller request.");
100
+ });
101
+ socket.on("error", () => undefined);
102
+ }
103
+ async function routeRequest(socket, line, token, dispatcher, stop) {
104
+ let request;
105
+ try {
106
+ request = parseControllerRequest(line);
107
+ }
108
+ catch (error) {
109
+ const protocolError = error instanceof ControllerProtocolError
110
+ ? error
111
+ : new ControllerProtocolError("INVALID_REQUEST", "Invalid controller request.");
112
+ sendResponse(socket, controllerFailure(protocolError.id, protocolError.code, protocolError.message));
113
+ return;
114
+ }
115
+ if (!tokensMatch(request.token, token)) {
116
+ sendResponse(socket, controllerFailure(request.id, "UNAUTHORIZED", "Controller authentication failed."));
117
+ return;
118
+ }
119
+ if (request.method === "controller.status") {
120
+ if (!isEmptyParams(request.params)) {
121
+ sendResponse(socket, controllerFailure(request.id, "INVALID_PARAMS", "Controller params are invalid."));
122
+ return;
123
+ }
124
+ sendResponse(socket, {
125
+ id: request.id,
126
+ ok: true,
127
+ result: { pid: process.pid, running: true }
128
+ });
129
+ return;
130
+ }
131
+ if (request.method === "controller.stop") {
132
+ if (!isEmptyParams(request.params)) {
133
+ sendResponse(socket, controllerFailure(request.id, "INVALID_PARAMS", "Controller params are invalid."));
134
+ return;
135
+ }
136
+ sendResponse(socket, { id: request.id, ok: true, result: { stopped: true } }, () => void stop());
137
+ return;
138
+ }
139
+ if (dispatcher === undefined) {
140
+ sendResponse(socket, controllerFailure(request.id, "METHOD_NOT_FOUND", "Controller method was not found."));
141
+ return;
142
+ }
143
+ try {
144
+ const result = await dispatcher(request.method, request.params);
145
+ sendResponse(socket, { id: request.id, ok: true, result });
146
+ }
147
+ catch (error) {
148
+ const safeError = safeDispatcherError(error);
149
+ sendResponse(socket, safeError === undefined
150
+ ? controllerFailure(request.id, "INTERNAL_ERROR", "Controller request failed.")
151
+ : controllerFailure(request.id, safeError.code, safeError.message));
152
+ }
153
+ }
154
+ function sendResponse(socket, response, onFlushed) {
155
+ let line;
156
+ try {
157
+ line = encodeControllerResponse(response);
158
+ }
159
+ catch {
160
+ line = encodeControllerResponse(controllerFailure(response.id, "INTERNAL_ERROR", "Controller request failed."));
161
+ }
162
+ socket.end(line, onFlushed);
163
+ }
164
+ function tokensMatch(left, right) {
165
+ const leftBytes = Buffer.from(left, "hex");
166
+ const rightBytes = Buffer.from(right, "hex");
167
+ return leftBytes.length === rightBytes.length && timingSafeEqual(leftBytes, rightBytes);
168
+ }
169
+ function listen(server, socketPath) {
170
+ return new Promise((resolve, reject) => {
171
+ const onError = (error) => {
172
+ server.off("listening", onListening);
173
+ reject(error);
174
+ };
175
+ const onListening = () => {
176
+ server.off("error", onError);
177
+ resolve();
178
+ };
179
+ server.once("error", onError);
180
+ server.once("listening", onListening);
181
+ server.listen(socketPath);
182
+ });
183
+ }
184
+ function socketIsReachable(socketPath) {
185
+ return new Promise((resolve) => {
186
+ const socket = createConnection(socketPath);
187
+ socket.once("connect", () => {
188
+ socket.destroy();
189
+ resolve(true);
190
+ });
191
+ socket.once("error", () => {
192
+ socket.destroy();
193
+ resolve(false);
194
+ });
195
+ });
196
+ }
197
+ function closeNetServer(server) {
198
+ if (!server.listening)
199
+ return Promise.resolve();
200
+ return new Promise((resolve, reject) => {
201
+ server.close((error) => error === undefined ? resolve() : reject(error));
202
+ });
203
+ }
204
+ async function removeOwnedDiscovery(path, token) {
205
+ try {
206
+ const value = JSON.parse(await readFile(path, "utf8"));
207
+ if (typeof value === "object"
208
+ && value !== null
209
+ && "token" in value
210
+ && value.token === token) {
211
+ await rm(path, { force: true });
212
+ }
213
+ }
214
+ catch {
215
+ // Cleanup is best effort; transport shutdown must still finish.
216
+ }
217
+ }
218
+ function safeDispatcherError(error) {
219
+ try {
220
+ if (!(error instanceof Error) || typeof error.message !== "string")
221
+ return undefined;
222
+ const message = safeErrorMessage(error.message);
223
+ if (message === undefined)
224
+ return undefined;
225
+ switch (error.name) {
226
+ case "CoreApplicationError": {
227
+ const code = "code" in error && typeof error.code === "string"
228
+ ? safeApplicationErrorCode(error.code)
229
+ : undefined;
230
+ return code === undefined ? undefined : { code, message };
231
+ }
232
+ case "CoreServiceError":
233
+ return { code: "SERVICE_ERROR", message };
234
+ case "CoreJobError":
235
+ return { code: "JOB_ERROR", message };
236
+ default:
237
+ return undefined;
238
+ }
239
+ }
240
+ catch {
241
+ return undefined;
242
+ }
243
+ }
244
+ function safeApplicationErrorCode(code) {
245
+ switch (code) {
246
+ case "INVALID_PARAMS":
247
+ case "METHOD_NOT_FOUND":
248
+ case "NOT_FOUND":
249
+ return code;
250
+ default:
251
+ return undefined;
252
+ }
253
+ }
254
+ function safeErrorMessage(message) {
255
+ const safe = message
256
+ .replace(/[\r\n\u2028\u2029]+/gu, " ")
257
+ .trim()
258
+ .slice(0, 512);
259
+ return safe.length === 0 ? undefined : safe;
260
+ }
261
+ function controllerAlreadyRunning() {
262
+ return new Error("Controller is already running.");
263
+ }
264
+ function isAddressInUse(error) {
265
+ return isNodeError(error) && error.code === "EADDRINUSE";
266
+ }
267
+ function isNodeError(error) {
268
+ return error instanceof Error && "code" in error;
269
+ }
@@ -0,0 +1,169 @@
1
+ export const MAX_CONTROLLER_MESSAGE_BYTES = 1_048_576;
2
+ export const CONTROLLER_DISCOVERY_PATH = "runtime/controller.json";
3
+ export const CONTROLLER_SOCKET_PATH = "runtime/controller.sock";
4
+ export class ControllerProtocolError extends Error {
5
+ code;
6
+ id;
7
+ constructor(code, message, id = "invalid") {
8
+ super(message);
9
+ this.code = code;
10
+ this.id = id;
11
+ this.name = "ControllerProtocolError";
12
+ }
13
+ }
14
+ export function parseControllerRequest(line) {
15
+ if (Buffer.byteLength(line) > MAX_CONTROLLER_MESSAGE_BYTES) {
16
+ throw new ControllerProtocolError("MESSAGE_TOO_LARGE", "Controller message exceeds 1 MiB.");
17
+ }
18
+ try {
19
+ const value = JSON.parse(line);
20
+ if (!isRecord(value) || !hasExactKeys(value, ["id", "token", "method", "params"])) {
21
+ throw new Error("shape");
22
+ }
23
+ if (!isIdentifier(value.id)
24
+ || typeof value.token !== "string"
25
+ || !/^[a-f0-9]{64}$/u.test(value.token)
26
+ || typeof value.method !== "string"
27
+ || value.method.length > 128
28
+ || !/^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)*$/u.test(value.method)
29
+ || !isJsonValue(value.params)) {
30
+ throw new Error("fields");
31
+ }
32
+ return Object.freeze({
33
+ id: value.id,
34
+ token: value.token,
35
+ method: value.method,
36
+ params: value.params
37
+ });
38
+ }
39
+ catch (error) {
40
+ if (error instanceof ControllerProtocolError)
41
+ throw error;
42
+ throw new ControllerProtocolError("INVALID_REQUEST", "Invalid controller request.");
43
+ }
44
+ }
45
+ export function parseControllerResponse(line, expectedId) {
46
+ if (Buffer.byteLength(line) > MAX_CONTROLLER_MESSAGE_BYTES) {
47
+ throw invalidResponse();
48
+ }
49
+ try {
50
+ const value = JSON.parse(line);
51
+ if (!isRecord(value) || value.id !== expectedId || !isIdentifier(value.id)) {
52
+ throw new Error("identity");
53
+ }
54
+ if (value.ok === true) {
55
+ if (!hasExactKeys(value, ["id", "ok", "result"]) || !isJsonValue(value.result)) {
56
+ throw new Error("success");
57
+ }
58
+ return Object.freeze({ id: value.id, ok: true, result: value.result });
59
+ }
60
+ if (value.ok !== false
61
+ || !hasExactKeys(value, ["id", "ok", "error"])
62
+ || !isRecord(value.error)
63
+ || !hasExactKeys(value.error, ["code", "message"])
64
+ || typeof value.error.code !== "string"
65
+ || !/^[A-Z][A-Z0-9_]{0,63}$/u.test(value.error.code)
66
+ || typeof value.error.message !== "string"
67
+ || value.error.message.length === 0
68
+ || value.error.message.length > 512) {
69
+ throw new Error("failure");
70
+ }
71
+ return Object.freeze({
72
+ id: value.id,
73
+ ok: false,
74
+ error: Object.freeze({ code: value.error.code, message: value.error.message })
75
+ });
76
+ }
77
+ catch {
78
+ throw invalidResponse();
79
+ }
80
+ }
81
+ export function encodeControllerRequest(request) {
82
+ const line = JSON.stringify(request);
83
+ parseControllerRequest(line);
84
+ return appendNewline(line);
85
+ }
86
+ export function encodeControllerResponse(response) {
87
+ const line = JSON.stringify(response);
88
+ parseControllerResponse(line, response.id);
89
+ return appendNewline(line);
90
+ }
91
+ export function controllerFailure(id, code, message) {
92
+ return Object.freeze({
93
+ id,
94
+ ok: false,
95
+ error: Object.freeze({ code, message })
96
+ });
97
+ }
98
+ export function parseControllerDiscovery(value, expectedSocketPath) {
99
+ if (!isRecord(value)
100
+ || !hasExactKeys(value, ["pid", "socketPath", "token"])
101
+ || !Number.isSafeInteger(value.pid)
102
+ || value.pid < 1
103
+ || value.socketPath !== expectedSocketPath
104
+ || typeof value.token !== "string"
105
+ || !/^[a-f0-9]{64}$/u.test(value.token)) {
106
+ throw new ControllerProtocolError("CONTROLLER_DISCOVERY_INVALID", "Controller discovery is invalid.");
107
+ }
108
+ return Object.freeze({
109
+ pid: value.pid,
110
+ socketPath: value.socketPath,
111
+ token: value.token
112
+ });
113
+ }
114
+ export function isEmptyParams(value) {
115
+ return isRecord(value) && Reflect.ownKeys(value).length === 0;
116
+ }
117
+ function appendNewline(line) {
118
+ if (Buffer.byteLength(line) > MAX_CONTROLLER_MESSAGE_BYTES) {
119
+ throw new ControllerProtocolError("MESSAGE_TOO_LARGE", "Controller message exceeds 1 MiB.");
120
+ }
121
+ return `${line}\n`;
122
+ }
123
+ function invalidResponse() {
124
+ return new ControllerProtocolError("INVALID_RESPONSE", "Controller response is invalid.");
125
+ }
126
+ function isIdentifier(value) {
127
+ return typeof value === "string"
128
+ && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value);
129
+ }
130
+ function isRecord(value) {
131
+ return typeof value === "object" && value !== null && !Array.isArray(value);
132
+ }
133
+ function hasExactKeys(value, keys) {
134
+ const actual = Reflect.ownKeys(value);
135
+ return actual.length === keys.length
136
+ && actual.every((key) => typeof key === "string")
137
+ && keys.every((key) => Object.hasOwn(value, key));
138
+ }
139
+ function isJsonValue(value) {
140
+ const pending = [value];
141
+ const seen = new Set();
142
+ while (pending.length > 0) {
143
+ const current = pending.pop();
144
+ if (current === null
145
+ || typeof current === "string"
146
+ || typeof current === "boolean") {
147
+ continue;
148
+ }
149
+ if (typeof current === "number") {
150
+ if (!Number.isFinite(current))
151
+ return false;
152
+ continue;
153
+ }
154
+ if (typeof current !== "object")
155
+ return false;
156
+ if (seen.has(current))
157
+ return false;
158
+ seen.add(current);
159
+ if (Array.isArray(current)) {
160
+ pending.push(...current);
161
+ continue;
162
+ }
163
+ const prototype = Object.getPrototypeOf(current);
164
+ if (prototype !== Object.prototype && prototype !== null)
165
+ return false;
166
+ pending.push(...Object.values(current));
167
+ }
168
+ return true;
169
+ }
@@ -0,0 +1,42 @@
1
+ export function createDecision(id, taskId, title, rationale, now) {
2
+ const timestamp = now.toISOString();
3
+ return {
4
+ schemaVersion: 1,
5
+ id: requireSafeIdentity(id, "Decision id"),
6
+ taskId: requireSafeIdentity(taskId, "Task id"),
7
+ title: requireText(title, "Decision title"),
8
+ rationale: requireText(rationale, "Decision rationale"),
9
+ status: "active",
10
+ createdAt: timestamp,
11
+ updatedAt: timestamp
12
+ };
13
+ }
14
+ export function supersedeDecision(decision, reason, now) {
15
+ if (decision.status === "superseded") {
16
+ throw new Error(`Decision is already superseded: ${decision.id}.`);
17
+ }
18
+ const timestamp = now.toISOString();
19
+ return {
20
+ ...decision,
21
+ status: "superseded",
22
+ supersededReason: requireText(reason, "Decision supersede reason"),
23
+ supersededAt: timestamp,
24
+ updatedAt: timestamp
25
+ };
26
+ }
27
+ function requireSafeIdentity(value, label) {
28
+ const normalized = requireText(value, label);
29
+ if (["__proto__", "prototype", "constructor", ".", ".."].includes(normalized)
30
+ || /[\/\\\0]/.test(normalized)) {
31
+ throw new Error(`${label} is invalid.`);
32
+ }
33
+ return normalized;
34
+ }
35
+ function requireText(value, label) {
36
+ if (typeof value !== "string" || value.includes("\0"))
37
+ throw new Error(`${label} is invalid.`);
38
+ const normalized = value.trim();
39
+ if (normalized.length === 0)
40
+ throw new Error(`${label} is required.`);
41
+ return normalized;
42
+ }