dxcomplete 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/.env.example +11 -0
- package/README.md +215 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +212 -0
- package/dist/http/server.d.ts +7 -0
- package/dist/http/server.js +236 -0
- package/dist/http/service.d.ts +7 -0
- package/dist/http/service.js +725 -0
- package/dist/init.d.ts +13 -0
- package/dist/init.js +128 -0
- package/dist/install-manifest.d.ts +25 -0
- package/dist/install-manifest.js +96 -0
- package/dist/mcp/docs.d.ts +98 -0
- package/dist/mcp/docs.js +438 -0
- package/dist/mcp/server.d.ts +20 -0
- package/dist/mcp/server.js +2345 -0
- package/dist/package-root.d.ts +2 -0
- package/dist/package-root.js +28 -0
- package/dist/runtime/actor.d.ts +14 -0
- package/dist/runtime/actor.js +42 -0
- package/dist/runtime/auth.d.ts +162 -0
- package/dist/runtime/auth.js +394 -0
- package/dist/runtime/check.d.ts +7 -0
- package/dist/runtime/check.js +16 -0
- package/dist/runtime/config.d.ts +17 -0
- package/dist/runtime/config.js +93 -0
- package/dist/runtime/mongo.d.ts +9 -0
- package/dist/runtime/mongo.js +56 -0
- package/dist/runtime/records.d.ts +336 -0
- package/dist/runtime/records.js +1463 -0
- package/dist/runtime/workspace.d.ts +19 -0
- package/dist/runtime/workspace.js +102 -0
- package/dist/upgrade.d.ts +20 -0
- package/dist/upgrade.js +246 -0
- package/dist/validate.d.ts +10 -0
- package/dist/validate.js +119 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.js +12 -0
- package/docs/codex-integration.md +29 -0
- package/docs/cost-model.md +61 -0
- package/docs/decision-basis.md +57 -0
- package/docs/diagrams.md +31 -0
- package/docs/glossary.md +147 -0
- package/docs/index.md +60 -0
- package/docs/model.md +110 -0
- package/docs/open-questions.md +61 -0
- package/docs/roles.md +42 -0
- package/docs/taxonomy.md +96 -0
- package/docs/workflows.md +60 -0
- package/package.json +62 -0
- package/scripts/check-env-surface.mjs +136 -0
- package/scripts/check-public-copy.mjs +263 -0
- package/scripts/check-service-boundary.mjs +63 -0
- package/scripts/dogfood-work-order.mjs +506 -0
- package/scripts/smoke-mcp-http.mjs +3572 -0
- package/src/cli.ts +268 -0
- package/src/http/server.ts +314 -0
- package/src/http/service.ts +934 -0
- package/src/init.ts +227 -0
- package/src/install-manifest.ts +144 -0
- package/src/mcp/docs.ts +557 -0
- package/src/mcp/server.ts +3525 -0
- package/src/package-root.ts +31 -0
- package/src/runtime/actor.ts +61 -0
- package/src/runtime/auth.ts +673 -0
- package/src/runtime/check.ts +18 -0
- package/src/runtime/config.ts +128 -0
- package/src/runtime/mongo.ts +89 -0
- package/src/runtime/records.ts +2303 -0
- package/src/runtime/workspace.ts +155 -0
- package/src/upgrade.ts +356 -0
- package/src/validate.ts +139 -0
- package/src/version.ts +16 -0
- package/templates/github/workflows/dxcomplete.yml +16 -0
- package/templates/next/pages/api/auth/callback/google.js +12 -0
- package/templates/next/pages/api/dxcomplete/[...path].js +12 -0
- package/templates/next/pages/api/dxcomplete.js +12 -0
- package/templates/next/pages/api/mcp.js +12 -0
- package/templates/next/vercel.json +18 -0
- package/templates/process/README.md +38 -0
- package/templates/process/controls.yml +113 -0
- package/templates/process/cost-model.yml +71 -0
- package/templates/process/decision-basis.yml +53 -0
- package/templates/process/decisions/.gitkeep +1 -0
- package/templates/process/diagrams/00-decision-basis.mmd +24 -0
- package/templates/process/diagrams/00-overview.mmd +20 -0
- package/templates/process/diagrams/01-intake-triage.mmd +20 -0
- package/templates/process/diagrams/02-product-definition.mmd +14 -0
- package/templates/process/diagrams/03-engineering-execution.mmd +15 -0
- package/templates/process/diagrams/04-qa-verification.mmd +12 -0
- package/templates/process/diagrams/05-product-validation.mmd +12 -0
- package/templates/process/diagrams/06-change-release-control.mmd +16 -0
- package/templates/process/diagrams/07-deployment-operations.mmd +16 -0
- package/templates/process/diagrams/08-support-incident-management.mmd +16 -0
- package/templates/process/diagrams/09-problem-improvement.mmd +14 -0
- package/templates/process/diagrams/10-risk-control-management.mmd +14 -0
- package/templates/process/diagrams/11-audit-evidence-capture.mmd +13 -0
- package/templates/process/evidence/.gitkeep +1 -0
- package/templates/process/risks/.gitkeep +1 -0
- package/templates/process/roles.yml +96 -0
- package/templates/process/taxonomy.yml +514 -0
- package/templates/process/workflows.yml +210 -0
- package/website/.well-known/oauth-authorization-server +22 -0
- package/website/.well-known/oauth-protected-resource/api/dxcomplete/mcp +10 -0
- package/website/.well-known/oauth-protected-resource/api/mcp +10 -0
- package/website/README.md +12 -0
- package/website/app.js +36 -0
- package/website/flow.html +85 -0
- package/website/glossary.html +280 -0
- package/website/index.html +90 -0
- package/website/objects.html +287 -0
- package/website/outcomes.html +117 -0
- package/website/phase-build.html +101 -0
- package/website/phase-elicit.html +102 -0
- package/website/phase-go-live.html +103 -0
- package/website/phase-measure.html +93 -0
- package/website/phase-operate.html +102 -0
- package/website/phase-orient.html +92 -0
- package/website/phase-weigh.html +98 -0
- package/website/roles.html +52 -0
- package/website/styles.css +1169 -0
package/src/init.ts
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { copyFile, mkdir, readdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { INSTALL_MANIFEST_PATH, writeInstallManifest } from "./install-manifest.js";
|
|
4
|
+
import { fileExists, resolvePackageRoot } from "./package-root.js";
|
|
5
|
+
import { getUpgradeManagedFiles } from "./upgrade.js";
|
|
6
|
+
|
|
7
|
+
export type InitOptions = {
|
|
8
|
+
targetDir: string;
|
|
9
|
+
force?: boolean;
|
|
10
|
+
dryRun?: boolean;
|
|
11
|
+
includeGithubWorkflow?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type InitResult = {
|
|
15
|
+
targetDir: string;
|
|
16
|
+
written: string[];
|
|
17
|
+
skipped: string[];
|
|
18
|
+
planned: string[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type CopyOptions = Required<Pick<InitOptions, "force" | "dryRun">>;
|
|
22
|
+
|
|
23
|
+
export async function initProject(options: InitOptions): Promise<InitResult> {
|
|
24
|
+
const targetDir = path.resolve(options.targetDir);
|
|
25
|
+
const force = options.force ?? false;
|
|
26
|
+
const dryRun = options.dryRun ?? false;
|
|
27
|
+
const includeGithubWorkflow = options.includeGithubWorkflow ?? true;
|
|
28
|
+
const packageRoot = await resolvePackageRoot();
|
|
29
|
+
|
|
30
|
+
const result: InitResult = {
|
|
31
|
+
targetDir,
|
|
32
|
+
written: [],
|
|
33
|
+
skipped: [],
|
|
34
|
+
planned: []
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
await copyDirectory(
|
|
38
|
+
path.join(packageRoot, "docs"),
|
|
39
|
+
path.join(targetDir, "dxcomplete", "docs"),
|
|
40
|
+
{ force, dryRun },
|
|
41
|
+
result
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
await copyDirectory(
|
|
45
|
+
path.join(packageRoot, "templates", "process"),
|
|
46
|
+
path.join(targetDir, "dxcomplete", "process"),
|
|
47
|
+
{ force, dryRun },
|
|
48
|
+
result
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
await writeWorkspaceConfig(targetDir, { force, dryRun }, result);
|
|
52
|
+
|
|
53
|
+
await copyDirectory(
|
|
54
|
+
path.join(packageRoot, "templates", "next", "pages"),
|
|
55
|
+
path.join(targetDir, "pages"),
|
|
56
|
+
{ force, dryRun },
|
|
57
|
+
result
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
await copyFileIfAvailable(
|
|
61
|
+
path.join(packageRoot, "templates", "next", "vercel.json"),
|
|
62
|
+
path.join(targetDir, "vercel.json"),
|
|
63
|
+
{ force, dryRun },
|
|
64
|
+
result
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
if (includeGithubWorkflow) {
|
|
68
|
+
await copyDirectory(
|
|
69
|
+
path.join(packageRoot, "templates", "github", "workflows"),
|
|
70
|
+
path.join(targetDir, ".github", "workflows"),
|
|
71
|
+
{ force, dryRun },
|
|
72
|
+
result
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await writeScaffoldManifest(packageRoot, targetDir, { force, dryRun }, result);
|
|
77
|
+
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function writeScaffoldManifest(
|
|
82
|
+
packageRoot: string,
|
|
83
|
+
targetDir: string,
|
|
84
|
+
options: CopyOptions,
|
|
85
|
+
result: InitResult
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
const managedFiles = getUpgradeManagedFiles(packageRoot, targetDir);
|
|
88
|
+
const managedDestinations = new Set(managedFiles.map((file) => file.destinationRelative));
|
|
89
|
+
const skippedManagedFile = result.skipped.some((file) => managedDestinations.has(file));
|
|
90
|
+
|
|
91
|
+
if (skippedManagedFile && !options.force) {
|
|
92
|
+
result.skipped.push(INSTALL_MANIFEST_PATH);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (options.dryRun) {
|
|
97
|
+
result.planned.push(INSTALL_MANIFEST_PATH);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await writeInstallManifest(targetDir, managedFiles);
|
|
102
|
+
result.written.push(INSTALL_MANIFEST_PATH);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function copyFileIfAvailable(
|
|
106
|
+
sourcePath: string,
|
|
107
|
+
destinationPath: string,
|
|
108
|
+
options: CopyOptions,
|
|
109
|
+
result: InitResult
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
if (!(await fileExists(sourcePath))) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const relativeDestination = path.relative(result.targetDir, destinationPath);
|
|
116
|
+
const exists = await fileExists(destinationPath);
|
|
117
|
+
|
|
118
|
+
if (exists && !options.force) {
|
|
119
|
+
result.skipped.push(relativeDestination);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (options.dryRun) {
|
|
124
|
+
result.planned.push(relativeDestination);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await mkdir(path.dirname(destinationPath), { recursive: true });
|
|
129
|
+
await copyFile(sourcePath, destinationPath);
|
|
130
|
+
result.written.push(relativeDestination);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function writeWorkspaceConfig(
|
|
134
|
+
targetDir: string,
|
|
135
|
+
options: CopyOptions,
|
|
136
|
+
result: InitResult
|
|
137
|
+
): Promise<void> {
|
|
138
|
+
const destinationPath = path.join(targetDir, "dxcomplete", "workspace.json");
|
|
139
|
+
const relativeDestination = path.relative(result.targetDir, destinationPath);
|
|
140
|
+
const exists = await fileExists(destinationPath);
|
|
141
|
+
|
|
142
|
+
if (exists && !options.force) {
|
|
143
|
+
result.skipped.push(relativeDestination);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (options.dryRun) {
|
|
148
|
+
result.planned.push(relativeDestination);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const workspaceId = slugifyWorkspaceId(path.basename(targetDir)) || "dxcomplete-workspace";
|
|
153
|
+
const workspaceName = titleizeWorkspaceName(path.basename(targetDir)) || "DX Complete Workspace";
|
|
154
|
+
const content = `${JSON.stringify(
|
|
155
|
+
{
|
|
156
|
+
workspaceId,
|
|
157
|
+
name: workspaceName,
|
|
158
|
+
bootstrapMembers: []
|
|
159
|
+
},
|
|
160
|
+
null,
|
|
161
|
+
2
|
|
162
|
+
)}\n`;
|
|
163
|
+
|
|
164
|
+
await mkdir(path.dirname(destinationPath), { recursive: true });
|
|
165
|
+
await writeFile(destinationPath, content, "utf8");
|
|
166
|
+
result.written.push(relativeDestination);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function copyDirectory(
|
|
170
|
+
sourceDir: string,
|
|
171
|
+
destinationDir: string,
|
|
172
|
+
options: CopyOptions,
|
|
173
|
+
result: InitResult
|
|
174
|
+
): Promise<void> {
|
|
175
|
+
const entries = await readdir(sourceDir, { withFileTypes: true });
|
|
176
|
+
|
|
177
|
+
if (!options.dryRun) {
|
|
178
|
+
await mkdir(destinationDir, { recursive: true });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const entry of entries) {
|
|
182
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
183
|
+
const destinationPath = path.join(destinationDir, entry.name);
|
|
184
|
+
|
|
185
|
+
if (entry.isDirectory()) {
|
|
186
|
+
await copyDirectory(sourcePath, destinationPath, options, result);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!entry.isFile()) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const relativeDestination = path.relative(result.targetDir, destinationPath);
|
|
195
|
+
const exists = await fileExists(destinationPath);
|
|
196
|
+
|
|
197
|
+
if (exists && !options.force) {
|
|
198
|
+
result.skipped.push(relativeDestination);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (options.dryRun) {
|
|
203
|
+
result.planned.push(relativeDestination);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
await mkdir(path.dirname(destinationPath), { recursive: true });
|
|
208
|
+
await copyFile(sourcePath, destinationPath);
|
|
209
|
+
result.written.push(relativeDestination);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function slugifyWorkspaceId(value: string): string {
|
|
214
|
+
return value
|
|
215
|
+
.trim()
|
|
216
|
+
.toLowerCase()
|
|
217
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
218
|
+
.replace(/^-+|-+$/g, "");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function titleizeWorkspaceName(value: string): string {
|
|
222
|
+
return value
|
|
223
|
+
.trim()
|
|
224
|
+
.replace(/[-_]+/g, " ")
|
|
225
|
+
.replace(/\s+/g, " ")
|
|
226
|
+
.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
227
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileExists } from "./package-root.js";
|
|
5
|
+
import {
|
|
6
|
+
DXCOMPLETE_PACKAGE_VERSION,
|
|
7
|
+
WORKSPACE_COMPATIBILITY_VERSION
|
|
8
|
+
} from "./version.js";
|
|
9
|
+
|
|
10
|
+
export const INSTALL_MANIFEST_PATH = path.join("dxcomplete", ".install-manifest.json");
|
|
11
|
+
export const INSTALL_MANIFEST_SCHEMA_VERSION = 1;
|
|
12
|
+
|
|
13
|
+
export type UpgradeManagedFile = {
|
|
14
|
+
destinationRelative: string;
|
|
15
|
+
sourceRelative: string;
|
|
16
|
+
sourcePath: string;
|
|
17
|
+
destinationPath: string;
|
|
18
|
+
strategy: "copy" | "merge-vercel";
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type InstallManifest = {
|
|
22
|
+
schemaVersion: number;
|
|
23
|
+
packageVersion: string;
|
|
24
|
+
workspaceCompatibility: number;
|
|
25
|
+
installedAt: string;
|
|
26
|
+
managedFiles: Record<string, InstallManifestFile>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type InstallManifestFile = {
|
|
30
|
+
sha256: string;
|
|
31
|
+
source: string;
|
|
32
|
+
strategy: UpgradeManagedFile["strategy"];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export async function readInstallManifest(targetDir: string): Promise<InstallManifest | undefined> {
|
|
36
|
+
const manifestPath = path.join(targetDir, INSTALL_MANIFEST_PATH);
|
|
37
|
+
|
|
38
|
+
if (!(await fileExists(manifestPath))) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const parsed = JSON.parse(await readFile(manifestPath, "utf8")) as unknown;
|
|
43
|
+
return parseInstallManifest(parsed, manifestPath);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function writeInstallManifest(targetDir: string, files: UpgradeManagedFile[]): Promise<void> {
|
|
47
|
+
const managedFiles: Record<string, InstallManifestFile> = {};
|
|
48
|
+
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
if (!(await fileExists(file.destinationPath))) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
managedFiles[file.destinationRelative] = {
|
|
55
|
+
sha256: await hashFile(file.destinationPath),
|
|
56
|
+
source: file.sourceRelative,
|
|
57
|
+
strategy: file.strategy
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const manifest: InstallManifest = {
|
|
62
|
+
schemaVersion: INSTALL_MANIFEST_SCHEMA_VERSION,
|
|
63
|
+
packageVersion: DXCOMPLETE_PACKAGE_VERSION,
|
|
64
|
+
workspaceCompatibility: WORKSPACE_COMPATIBILITY_VERSION,
|
|
65
|
+
installedAt: new Date().toISOString(),
|
|
66
|
+
managedFiles
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const manifestPath = path.join(targetDir, INSTALL_MANIFEST_PATH);
|
|
70
|
+
await mkdir(path.dirname(manifestPath), { recursive: true });
|
|
71
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function hashFile(filePath: string): Promise<string> {
|
|
75
|
+
return hashContent(await readFile(filePath));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function hashContent(content: string | Buffer): string {
|
|
79
|
+
return createHash("sha256").update(content).digest("hex");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseInstallManifest(value: unknown, source: string): InstallManifest {
|
|
83
|
+
if (!value || typeof value !== "object") {
|
|
84
|
+
throw new Error(`${source} must be a JSON object.`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const input = value as Record<string, unknown>;
|
|
88
|
+
const schemaVersion = readNumber(input.schemaVersion, "schemaVersion", source);
|
|
89
|
+
const packageVersion = readString(input.packageVersion, "packageVersion", source);
|
|
90
|
+
const workspaceCompatibility = readNumber(input.workspaceCompatibility, "workspaceCompatibility", source);
|
|
91
|
+
const installedAt = readString(input.installedAt, "installedAt", source);
|
|
92
|
+
const managedFiles = parseManagedFiles(input.managedFiles, source);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
schemaVersion,
|
|
96
|
+
packageVersion,
|
|
97
|
+
workspaceCompatibility,
|
|
98
|
+
installedAt,
|
|
99
|
+
managedFiles
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseManagedFiles(value: unknown, source: string): Record<string, InstallManifestFile> {
|
|
104
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
105
|
+
throw new Error(`${source} managedFiles must be a JSON object.`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const output: Record<string, InstallManifestFile> = {};
|
|
109
|
+
for (const [filePath, fileValue] of Object.entries(value as Record<string, unknown>)) {
|
|
110
|
+
if (!fileValue || typeof fileValue !== "object" || Array.isArray(fileValue)) {
|
|
111
|
+
throw new Error(`${source} managedFiles.${filePath} must be a JSON object.`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const file = fileValue as Record<string, unknown>;
|
|
115
|
+
const strategy = readString(file.strategy, `managedFiles.${filePath}.strategy`, source);
|
|
116
|
+
if (strategy !== "copy" && strategy !== "merge-vercel") {
|
|
117
|
+
throw new Error(`${source} managedFiles.${filePath}.strategy must be copy or merge-vercel.`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
output[filePath] = {
|
|
121
|
+
sha256: readString(file.sha256, `managedFiles.${filePath}.sha256`, source),
|
|
122
|
+
source: readString(file.source, `managedFiles.${filePath}.source`, source),
|
|
123
|
+
strategy
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return output;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function readString(value: unknown, key: string, source: string): string {
|
|
131
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
132
|
+
throw new Error(`${source} ${key} must be a non-empty string.`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function readNumber(value: unknown, key: string, source: string): number {
|
|
139
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
140
|
+
throw new Error(`${source} ${key} must be a non-negative integer.`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return value;
|
|
144
|
+
}
|