human-to-code 0.0.1 → 0.1.2

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 (134) hide show
  1. package/Readme.md +368 -38
  2. package/SECURITY.md +175 -11
  3. package/dist/adapters/general.d.ts +30 -0
  4. package/dist/adapters/general.d.ts.map +1 -0
  5. package/dist/adapters/general.js +77 -0
  6. package/dist/adapters/general.js.map +1 -0
  7. package/dist/adapters/node.d.ts +8 -0
  8. package/dist/adapters/node.d.ts.map +1 -0
  9. package/dist/adapters/node.js +1071 -0
  10. package/dist/adapters/node.js.map +1 -0
  11. package/dist/adapters/python.d.ts +7 -0
  12. package/dist/adapters/python.d.ts.map +1 -0
  13. package/dist/adapters/python.js +540 -0
  14. package/dist/adapters/python.js.map +1 -0
  15. package/dist/adapters/rust.d.ts +7 -0
  16. package/dist/adapters/rust.d.ts.map +1 -0
  17. package/dist/adapters/rust.js +517 -0
  18. package/dist/adapters/rust.js.map +1 -0
  19. package/dist/analyzer-types.d.ts +161 -0
  20. package/dist/analyzer-types.d.ts.map +1 -0
  21. package/dist/analyzer-types.js +8 -0
  22. package/dist/analyzer-types.js.map +1 -0
  23. package/dist/analyzer-utils.d.ts +40 -0
  24. package/dist/analyzer-utils.d.ts.map +1 -0
  25. package/dist/analyzer-utils.js +591 -0
  26. package/dist/analyzer-utils.js.map +1 -0
  27. package/dist/analyzer.d.ts +21 -0
  28. package/dist/analyzer.d.ts.map +1 -0
  29. package/dist/analyzer.js +111 -0
  30. package/dist/analyzer.js.map +1 -0
  31. package/dist/certification.d.ts +102 -0
  32. package/dist/certification.d.ts.map +1 -0
  33. package/dist/certification.js +193 -0
  34. package/dist/certification.js.map +1 -0
  35. package/dist/cli.d.ts +4 -0
  36. package/dist/cli.d.ts.map +1 -0
  37. package/dist/cli.js +566 -0
  38. package/dist/cli.js.map +1 -0
  39. package/dist/compiler-skills.d.ts +25 -0
  40. package/dist/compiler-skills.d.ts.map +1 -0
  41. package/dist/compiler-skills.js +158 -0
  42. package/dist/compiler-skills.js.map +1 -0
  43. package/dist/compiler-tools.d.ts +29 -0
  44. package/dist/compiler-tools.d.ts.map +1 -0
  45. package/dist/compiler-tools.js +337 -0
  46. package/dist/compiler-tools.js.map +1 -0
  47. package/dist/config.d.ts +122 -0
  48. package/dist/config.d.ts.map +1 -0
  49. package/dist/config.js +704 -0
  50. package/dist/config.js.map +1 -0
  51. package/dist/context.d.ts +153 -0
  52. package/dist/context.d.ts.map +1 -0
  53. package/dist/context.js +772 -0
  54. package/dist/context.js.map +1 -0
  55. package/dist/contracts.d.ts +196 -0
  56. package/dist/contracts.d.ts.map +1 -0
  57. package/dist/contracts.js +721 -0
  58. package/dist/contracts.js.map +1 -0
  59. package/dist/discovery.d.ts +34 -0
  60. package/dist/discovery.d.ts.map +1 -0
  61. package/dist/discovery.js +312 -0
  62. package/dist/discovery.js.map +1 -0
  63. package/dist/documentation.d.ts +44 -0
  64. package/dist/documentation.d.ts.map +1 -0
  65. package/dist/documentation.js +418 -0
  66. package/dist/documentation.js.map +1 -0
  67. package/dist/index.d.ts +23 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +21 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/patch.d.ts +71 -0
  72. package/dist/patch.d.ts.map +1 -0
  73. package/dist/patch.js +394 -0
  74. package/dist/patch.js.map +1 -0
  75. package/dist/pinned-http.d.ts +15 -0
  76. package/dist/pinned-http.d.ts.map +1 -0
  77. package/dist/pinned-http.js +170 -0
  78. package/dist/pinned-http.js.map +1 -0
  79. package/dist/planner.d.ts +30 -0
  80. package/dist/planner.d.ts.map +1 -0
  81. package/dist/planner.js +228 -0
  82. package/dist/planner.js.map +1 -0
  83. package/dist/provider.d.ts +168 -0
  84. package/dist/provider.d.ts.map +1 -0
  85. package/dist/provider.js +512 -0
  86. package/dist/provider.js.map +1 -0
  87. package/dist/providers.d.ts +41 -0
  88. package/dist/providers.d.ts.map +1 -0
  89. package/dist/providers.js +954 -0
  90. package/dist/providers.js.map +1 -0
  91. package/dist/run-store.d.ts +24 -0
  92. package/dist/run-store.d.ts.map +1 -0
  93. package/dist/run-store.js +285 -0
  94. package/dist/run-store.js.map +1 -0
  95. package/dist/schemas.d.ts +18 -0
  96. package/dist/schemas.d.ts.map +1 -0
  97. package/dist/schemas.js +228 -0
  98. package/dist/schemas.js.map +1 -0
  99. package/dist/secret-scan.d.ts +30 -0
  100. package/dist/secret-scan.d.ts.map +1 -0
  101. package/dist/secret-scan.js +122 -0
  102. package/dist/secret-scan.js.map +1 -0
  103. package/dist/snapshot.d.ts +33 -0
  104. package/dist/snapshot.d.ts.map +1 -0
  105. package/dist/snapshot.js +136 -0
  106. package/dist/snapshot.js.map +1 -0
  107. package/dist/support-matrix.d.ts +23 -0
  108. package/dist/support-matrix.d.ts.map +1 -0
  109. package/dist/support-matrix.js +199 -0
  110. package/dist/support-matrix.js.map +1 -0
  111. package/dist/types.d.ts +63 -0
  112. package/dist/types.d.ts.map +1 -0
  113. package/dist/types.js +8 -0
  114. package/dist/types.js.map +1 -0
  115. package/dist/validation.d.ts +25 -0
  116. package/dist/validation.d.ts.map +1 -0
  117. package/dist/validation.js +388 -0
  118. package/dist/validation.js.map +1 -0
  119. package/dist/workflow.d.ts +65 -0
  120. package/dist/workflow.d.ts.map +1 -0
  121. package/dist/workflow.js +1636 -0
  122. package/dist/workflow.js.map +1 -0
  123. package/package.json +36 -9
  124. package/.claude/settings.local.json +0 -12
  125. package/.github/workflows/ci.yml +0 -18
  126. package/CONTRIBUTING.md +0 -40
  127. package/src/cli.ts +0 -218
  128. package/src/config.ts +0 -186
  129. package/src/discovery.ts +0 -186
  130. package/src/index.ts +0 -22
  131. package/src/types.ts +0 -74
  132. package/test/config.test.ts +0 -97
  133. package/test/discovery.test.ts +0 -91
  134. package/tsconfig.json +0 -17
package/dist/config.js ADDED
@@ -0,0 +1,704 @@
1
+ /**
2
+ * Strict, versioned configuration.
3
+ *
4
+ * Configuration is JSON and is never interpreted by a model. Unknown fields
5
+ * are rejected so misspellings and future/legacy semantics cannot silently
6
+ * weaken a run. Credentials are deliberately not part of the schema: provider
7
+ * adapters must read them from their provider-specific environment variables.
8
+ */
9
+ import { constants as fsConstants } from "node:fs";
10
+ import { lstat, open } from "node:fs/promises";
11
+ import { isIP } from "node:net";
12
+ import { isAbsolute, join } from "node:path";
13
+ export const CONFIG_FILENAME = "human-to-code.config.json";
14
+ export const CONFIG_SCHEMA_VERSION = 1;
15
+ const MAX_CONFIG_BYTES = 1024 * 1024;
16
+ const LANGUAGES = [
17
+ "typescript",
18
+ "javascript",
19
+ "python",
20
+ "rust",
21
+ ];
22
+ const PROVIDERS = [
23
+ "openai",
24
+ "anthropic",
25
+ "ollama",
26
+ "grok",
27
+ "gemini",
28
+ ];
29
+ /** Stable defaults for this package release. They are not model aliases. */
30
+ const DEFAULT_MODEL = Object.freeze({
31
+ anthropic: "claude-opus-4-8",
32
+ openai: "gpt-4o",
33
+ ollama: "qwen2.5-coder:7b",
34
+ grok: "grok-4.1",
35
+ gemini: "gemini-2.5-pro",
36
+ });
37
+ function deepFreeze(value) {
38
+ if (typeof value !== "object" || value === null || Object.isFrozen(value)) {
39
+ return value;
40
+ }
41
+ for (const nested of Object.values(value))
42
+ deepFreeze(nested);
43
+ return Object.freeze(value);
44
+ }
45
+ const DEFAULT_CONFIG_VALUE = {
46
+ schemaVersion: CONFIG_SCHEMA_VERSION,
47
+ language: "typescript",
48
+ filesToIgnore: ["node_modules", ".git", "dist"],
49
+ allowNonHumanFiles: false,
50
+ provider: {
51
+ // A fresh `npx human-to-code .` installation starts with a loopback-only
52
+ // provider. Remote code transmission is never an implicit default.
53
+ name: "ollama",
54
+ model: DEFAULT_MODEL.ollama,
55
+ },
56
+ workspaces: [],
57
+ documentation: {
58
+ mode: "local-first",
59
+ privatePaths: [],
60
+ officialDomains: [],
61
+ officialSources: [],
62
+ },
63
+ privacy: {
64
+ remoteProviderConsent: false,
65
+ telemetry: false,
66
+ excludedPaths: [],
67
+ maxFileBytes: 512_000,
68
+ maxContextTokens: 64_000,
69
+ },
70
+ sandbox: {
71
+ required: true,
72
+ engine: "auto",
73
+ network: "none",
74
+ },
75
+ budgets: {
76
+ maxCostUsd: 10,
77
+ // Cumulative run ceilings. Preflight accounting uses a pessimistic
78
+ // tokenizer-independent upper bound and charges failed remote attempts.
79
+ maxInputTokens: 2_000_000,
80
+ maxOutputTokens: 120_000,
81
+ maxRequests: 12,
82
+ maxRepairs: 2,
83
+ timeoutMs: 900_000,
84
+ },
85
+ };
86
+ /** Frozen defaults. Callers receive a deep clone, never this object. */
87
+ export const DEFAULT_CONFIG = deepFreeze(DEFAULT_CONFIG_VALUE);
88
+ /** Thrown for malformed or unsafe config; messages are safe to show users. */
89
+ export class ConfigError extends Error {
90
+ name = "ConfigError";
91
+ }
92
+ /** The configured default model id for a provider in this package release. */
93
+ export function defaultModelFor(name) {
94
+ return DEFAULT_MODEL[name];
95
+ }
96
+ function cloneDefaults() {
97
+ return structuredClone(DEFAULT_CONFIG);
98
+ }
99
+ function isObject(value) {
100
+ return typeof value === "object" && value !== null && !Array.isArray(value);
101
+ }
102
+ function field(path, key) {
103
+ return path.length === 0 ? key : `${path}.${key}`;
104
+ }
105
+ function assertKnownKeys(value, allowed, path) {
106
+ const allowedSet = new Set(allowed);
107
+ for (const key of Object.keys(value)) {
108
+ if (!allowedSet.has(key)) {
109
+ throw new ConfigError(`Unknown configuration field \`${field(path, key)}\`.`);
110
+ }
111
+ }
112
+ }
113
+ const CREDENTIAL_KEY_SUFFIXES = [
114
+ "apikey",
115
+ "accesstoken",
116
+ "authtoken",
117
+ "bearertoken",
118
+ "clientsecret",
119
+ "password",
120
+ "passphrase",
121
+ "credential",
122
+ "credentials",
123
+ "privatekey",
124
+ "authorization",
125
+ ];
126
+ function rejectCredentialKeys(value, path = "") {
127
+ if (Array.isArray(value)) {
128
+ value.forEach((entry, index) => rejectCredentialKeys(entry, `${path}[${index}]`));
129
+ return;
130
+ }
131
+ if (!isObject(value))
132
+ return;
133
+ for (const [key, nested] of Object.entries(value)) {
134
+ const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
135
+ if (normalized !== "apikeyenv" &&
136
+ (normalized === "key" ||
137
+ normalized === "secret" ||
138
+ normalized === "token" ||
139
+ normalized.endsWith("secret") ||
140
+ CREDENTIAL_KEY_SUFFIXES.some((suffix) => normalized.endsWith(suffix)))) {
141
+ throw new ConfigError(`Credential-like field \`${field(path, key)}\` is prohibited; use the provider-specific environment variable.`);
142
+ }
143
+ rejectCredentialKeys(nested, field(path, key));
144
+ }
145
+ }
146
+ function expectObject(value, path) {
147
+ if (!isObject(value))
148
+ throw new ConfigError(`\`${path}\` must be an object.`);
149
+ return value;
150
+ }
151
+ function expectBoolean(value, path) {
152
+ if (typeof value !== "boolean") {
153
+ throw new ConfigError(`\`${path}\` must be a boolean.`);
154
+ }
155
+ return value;
156
+ }
157
+ function expectString(value, path, maxLength = 512) {
158
+ if (typeof value !== "string" ||
159
+ value.length === 0 ||
160
+ value.trim() !== value ||
161
+ value.length > maxLength ||
162
+ value.includes("\0")) {
163
+ throw new ConfigError(`\`${path}\` must be a non-empty, trimmed string of at most ${maxLength} characters.`);
164
+ }
165
+ return value;
166
+ }
167
+ function expectNumber(value, path, minimum, maximum, integer = false) {
168
+ if (typeof value !== "number" ||
169
+ !Number.isFinite(value) ||
170
+ value < minimum ||
171
+ value > maximum ||
172
+ (integer && !Number.isInteger(value))) {
173
+ throw new ConfigError(`\`${path}\` must be ${integer ? "an integer" : "a number"} between ${minimum} and ${maximum}.`);
174
+ }
175
+ return value;
176
+ }
177
+ function expectStringArray(value, path, validator = expectString) {
178
+ if (!Array.isArray(value)) {
179
+ throw new ConfigError(`\`${path}\` must be an array of strings.`);
180
+ }
181
+ const result = value.map((item, index) => {
182
+ if (typeof item !== "string") {
183
+ throw new ConfigError(`\`${path}[${index}]\` must be a string.`);
184
+ }
185
+ return validator(item, `${path}[${index}]`);
186
+ });
187
+ if (new Set(result).size !== result.length) {
188
+ throw new ConfigError(`\`${path}\` must not contain duplicates.`);
189
+ }
190
+ return result;
191
+ }
192
+ function validateRelativePath(value, path, allowRoot = false) {
193
+ expectString(value, path, 1024);
194
+ if (isAbsolute(value) ||
195
+ /^[a-zA-Z]:/.test(value) ||
196
+ value.startsWith("\\") ||
197
+ value.includes("\\")) {
198
+ throw new ConfigError(`\`${path}\` must be a portable repository-relative path.`);
199
+ }
200
+ if (allowRoot && value === ".")
201
+ return value;
202
+ const segments = value.split("/");
203
+ if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
204
+ throw new ConfigError(`\`${path}\` must not contain empty, dot, or parent segments.`);
205
+ }
206
+ return value;
207
+ }
208
+ function validateIgnoreName(value, path) {
209
+ expectString(value, path, 255);
210
+ if (value === "." || value === ".." || value.includes("/") || value.includes("\\")) {
211
+ throw new ConfigError(`\`${path}\` must be a file or directory name, not a path.`);
212
+ }
213
+ return value;
214
+ }
215
+ function isLoopback(hostname) {
216
+ const host = hostname.replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase();
217
+ if (host === "localhost" || host === "::1")
218
+ return true;
219
+ if (isIP(host) === 4)
220
+ return host.startsWith("127.");
221
+ return false;
222
+ }
223
+ function isUnsafeLiteralAddress(hostname) {
224
+ const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
225
+ const family = isIP(host);
226
+ if (family === 4) {
227
+ const octets = host.split(".").map(Number);
228
+ const first = octets[0] ?? -1;
229
+ const second = octets[1] ?? -1;
230
+ return (first === 0 ||
231
+ first === 10 ||
232
+ first === 127 ||
233
+ (first === 100 && second >= 64 && second <= 127) ||
234
+ (first === 169 && second === 254) ||
235
+ (first === 172 && second >= 16 && second <= 31) ||
236
+ (first === 192 && second === 168) ||
237
+ first >= 224);
238
+ }
239
+ if (family === 6) {
240
+ return (host === "::" ||
241
+ host === "::1" ||
242
+ host.startsWith("::ffff:") ||
243
+ host.startsWith("fc") ||
244
+ host.startsWith("fd") ||
245
+ /^fe[89ab]/.test(host) ||
246
+ host.startsWith("ff"));
247
+ }
248
+ return false;
249
+ }
250
+ /** Validate a custom endpoint without performing any network access. */
251
+ export function validateProviderBaseUrl(raw, provider, trusted) {
252
+ const value = expectString(raw, "provider.baseUrl", 2048);
253
+ if (!trusted) {
254
+ throw new ConfigError("`provider.trustCustomEndpoint` must be true when `provider.baseUrl` is set.");
255
+ }
256
+ let url;
257
+ if (!value.startsWith("https://") && !value.startsWith("http://")) {
258
+ throw new ConfigError("`provider.baseUrl` must use an explicit lowercase URL scheme.");
259
+ }
260
+ try {
261
+ url = new URL(value);
262
+ }
263
+ catch {
264
+ throw new ConfigError("`provider.baseUrl` must be an absolute URL.");
265
+ }
266
+ if (url.username !== "" || url.password !== "") {
267
+ throw new ConfigError("`provider.baseUrl` must not contain credentials.");
268
+ }
269
+ if (url.search !== "" || url.hash !== "") {
270
+ throw new ConfigError("`provider.baseUrl` must not contain a query or fragment.");
271
+ }
272
+ const loopback = isLoopback(url.hostname);
273
+ if (url.protocol === "http:") {
274
+ if (provider !== "ollama" || !loopback) {
275
+ throw new ConfigError("Plain HTTP is permitted only for an explicitly trusted Ollama loopback endpoint.");
276
+ }
277
+ }
278
+ else if (url.protocol !== "https:") {
279
+ throw new ConfigError("`provider.baseUrl` must use https:// (or Ollama HTTP loopback).");
280
+ }
281
+ const hostname = url.hostname.replace(/\.$/, "").toLowerCase();
282
+ if ((!loopback && isUnsafeLiteralAddress(hostname)) ||
283
+ (!loopback &&
284
+ (hostname.endsWith(".localhost") ||
285
+ hostname.endsWith(".local") ||
286
+ hostname.endsWith(".internal")))) {
287
+ throw new ConfigError("`provider.baseUrl` must not target a private network.");
288
+ }
289
+ if (loopback && provider !== "ollama") {
290
+ throw new ConfigError("Only the local Ollama provider may use a loopback endpoint.");
291
+ }
292
+ if (isIP(hostname) === 0 && !loopback && !hostname.includes(".")) {
293
+ throw new ConfigError("`provider.baseUrl` hostname must be a fully qualified domain.");
294
+ }
295
+ return value;
296
+ }
297
+ function validateProvider(raw, path = "provider") {
298
+ const value = expectObject(raw, path);
299
+ assertKnownKeys(value, ["name", "model", "baseUrl", "trustCustomEndpoint", "apiKeyEnv", "pricing"], path);
300
+ const name = value.name;
301
+ if (typeof name !== "string" || !PROVIDERS.includes(name)) {
302
+ throw new ConfigError(`\`${path}.name\` must be one of: ${PROVIDERS.join(", ")}.`);
303
+ }
304
+ const providerName = name;
305
+ const model = value.model === undefined
306
+ ? DEFAULT_MODEL[providerName]
307
+ : expectString(value.model, `${path}.model`, 256);
308
+ const result = { name: providerName, model };
309
+ if (value.pricing !== undefined) {
310
+ const pricing = expectObject(value.pricing, `${path}.pricing`);
311
+ assertKnownKeys(pricing, ["inputUsdPerMillionTokens", "outputUsdPerMillionTokens", "unmetered"], `${path}.pricing`);
312
+ const inputUsdPerMillionTokens = expectNumber(pricing.inputUsdPerMillionTokens, `${path}.pricing.inputUsdPerMillionTokens`, 0, 1_000_000);
313
+ const outputUsdPerMillionTokens = expectNumber(pricing.outputUsdPerMillionTokens, `${path}.pricing.outputUsdPerMillionTokens`, 0, 1_000_000);
314
+ if (pricing.unmetered !== undefined && pricing.unmetered !== true) {
315
+ throw new ConfigError(`\`${path}.pricing.unmetered\` may only be true.`);
316
+ }
317
+ const bothZero = inputUsdPerMillionTokens === 0 && outputUsdPerMillionTokens === 0;
318
+ if (bothZero !== (pricing.unmetered === true)) {
319
+ throw new ConfigError(`\`${path}.pricing.unmetered\` must be true exactly when both configured rates are zero.`);
320
+ }
321
+ result.pricing = {
322
+ inputUsdPerMillionTokens,
323
+ outputUsdPerMillionTokens,
324
+ ...(pricing.unmetered === true ? { unmetered: true } : {}),
325
+ };
326
+ }
327
+ if (value.apiKeyEnv !== undefined) {
328
+ if (typeof value.apiKeyEnv !== "string" ||
329
+ !/^[A-Z_][A-Z0-9_]{0,127}$/.test(value.apiKeyEnv)) {
330
+ throw new ConfigError(`\`${path}.apiKeyEnv\` must be an uppercase environment-variable name.`);
331
+ }
332
+ result.apiKeyEnv = value.apiKeyEnv;
333
+ }
334
+ if (value.trustCustomEndpoint !== undefined && value.trustCustomEndpoint !== true) {
335
+ throw new ConfigError(`\`${path}.trustCustomEndpoint\` may only be true.`);
336
+ }
337
+ if (value.baseUrl !== undefined) {
338
+ if (typeof value.baseUrl !== "string") {
339
+ throw new ConfigError(`\`${path}.baseUrl\` must be a string.`);
340
+ }
341
+ result.baseUrl = validateProviderBaseUrl(value.baseUrl, providerName, value.trustCustomEndpoint === true);
342
+ result.trustCustomEndpoint = true;
343
+ const endpoint = new URL(result.baseUrl);
344
+ const officialOllama = providerName === "ollama" &&
345
+ endpoint.protocol === "https:" &&
346
+ endpoint.hostname === "ollama.com";
347
+ if (endpoint.protocol === "http:" && result.apiKeyEnv !== undefined) {
348
+ throw new ConfigError(`\`${path}.apiKeyEnv\` is not allowed for a local HTTP Ollama endpoint.`);
349
+ }
350
+ if (officialOllama && result.apiKeyEnv === undefined) {
351
+ result.apiKeyEnv = "OLLAMA_API_KEY";
352
+ }
353
+ }
354
+ else if (value.trustCustomEndpoint !== undefined) {
355
+ throw new ConfigError(`\`${path}.trustCustomEndpoint\` is only valid with \`${path}.baseUrl\`.`);
356
+ }
357
+ return result;
358
+ }
359
+ function validateDocumentation(raw, path, partial) {
360
+ const value = expectObject(raw, path);
361
+ assertKnownKeys(value, ["mode", "privatePaths", "officialDomains", "officialSources"], path);
362
+ const result = {};
363
+ if (!partial || value.mode !== undefined) {
364
+ const mode = value.mode;
365
+ if (mode !== "local-first" && mode !== "offline") {
366
+ throw new ConfigError(`\`${path}.mode\` must be local-first or offline.`);
367
+ }
368
+ result.mode = mode;
369
+ }
370
+ if (!partial || value.privatePaths !== undefined) {
371
+ result.privatePaths = expectStringArray(value.privatePaths, `${path}.privatePaths`, validateRelativePath);
372
+ }
373
+ if (!partial || value.officialDomains !== undefined) {
374
+ result.officialDomains = expectStringArray(value.officialDomains, `${path}.officialDomains`, (domain, itemPath) => {
375
+ expectString(domain, itemPath, 253);
376
+ if (domain !== domain.toLowerCase() ||
377
+ domain.includes(":") ||
378
+ domain.includes("/") ||
379
+ domain.startsWith(".") ||
380
+ domain.endsWith(".") ||
381
+ domain.includes("*") ||
382
+ !domain.includes(".") ||
383
+ isIP(domain) !== 0) {
384
+ throw new ConfigError(`\`${itemPath}\` must be a lowercase public domain.`);
385
+ }
386
+ return domain;
387
+ });
388
+ }
389
+ if (!partial || value.officialSources !== undefined) {
390
+ if (!Array.isArray(value.officialSources)) {
391
+ throw new ConfigError(`\`${path}.officialSources\` must be an array.`);
392
+ }
393
+ if (value.officialSources.length > 100) {
394
+ throw new ConfigError(`\`${path}.officialSources\` may contain at most 100 entries.`);
395
+ }
396
+ const sources = value.officialSources.map((raw, index) => {
397
+ const itemPath = `${path}.officialSources[${index}]`;
398
+ const source = expectObject(raw, itemPath);
399
+ assertKnownKeys(source, ["ecosystem", "dependency", "version", "url"], itemPath);
400
+ if (!["react", "nestjs", "fastapi", "rust"].includes(String(source.ecosystem))) {
401
+ throw new ConfigError(`\`${itemPath}.ecosystem\` is not a supported ecosystem.`);
402
+ }
403
+ const dependency = expectString(source.dependency, `${itemPath}.dependency`, 256);
404
+ if (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*$/iu.test(dependency)) {
405
+ throw new ConfigError(`\`${itemPath}.dependency\` must be one exact dependency name.`);
406
+ }
407
+ const version = expectString(source.version, `${itemPath}.version`, 128);
408
+ if (!/^[A-Za-z0-9][A-Za-z0-9._+:-]{0,127}$/u.test(version)
409
+ || /^(?:latest|next|stable|nightly|main|master|head|dev)$/iu.test(version)
410
+ || /[<>=^~*]/u.test(version)) {
411
+ throw new ConfigError(`\`${itemPath}.version\` must be one exact version identifier.`);
412
+ }
413
+ const url = expectString(source.url, `${itemPath}.url`, 2048);
414
+ let parsed;
415
+ try {
416
+ parsed = new URL(url);
417
+ }
418
+ catch {
419
+ throw new ConfigError(`\`${itemPath}.url\` must be an absolute HTTPS URL.`);
420
+ }
421
+ if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.port || parsed.hash) {
422
+ throw new ConfigError(`\`${itemPath}.url\` must be credential-free HTTPS on the default port without a fragment.`);
423
+ }
424
+ let versionedLocation;
425
+ try {
426
+ versionedLocation = decodeURIComponent(`${parsed.pathname}${parsed.search}`);
427
+ }
428
+ catch {
429
+ throw new ConfigError(`\`${itemPath}.url\` contains invalid percent encoding.`);
430
+ }
431
+ if (!versionedLocation.includes(version)) {
432
+ throw new ConfigError(`\`${itemPath}.url\` must visibly bind the exact configured version in its path or query.`);
433
+ }
434
+ return {
435
+ ecosystem: source.ecosystem,
436
+ dependency,
437
+ version,
438
+ url,
439
+ };
440
+ });
441
+ const identities = sources.map((source) => `${source.ecosystem}\0${source.dependency.toLowerCase()}\0${source.version}`);
442
+ if (new Set(identities).size !== identities.length) {
443
+ throw new ConfigError(`\`${path}.officialSources\` must not contain duplicate ecosystem/dependency/version mappings.`);
444
+ }
445
+ result.officialSources = sources;
446
+ }
447
+ return result;
448
+ }
449
+ function validatePrivacy(raw, path, partial) {
450
+ const value = expectObject(raw, path);
451
+ assertKnownKeys(value, [
452
+ "remoteProviderConsent",
453
+ "telemetry",
454
+ "excludedPaths",
455
+ "maxFileBytes",
456
+ "maxContextTokens",
457
+ ], path);
458
+ const result = {};
459
+ if (!partial || value.remoteProviderConsent !== undefined) {
460
+ result.remoteProviderConsent = expectBoolean(value.remoteProviderConsent, `${path}.remoteProviderConsent`);
461
+ }
462
+ if (!partial || value.telemetry !== undefined) {
463
+ result.telemetry = expectBoolean(value.telemetry, `${path}.telemetry`);
464
+ }
465
+ if (!partial || value.excludedPaths !== undefined) {
466
+ result.excludedPaths = expectStringArray(value.excludedPaths, `${path}.excludedPaths`, validateRelativePath);
467
+ }
468
+ if (!partial || value.maxFileBytes !== undefined) {
469
+ result.maxFileBytes = expectNumber(value.maxFileBytes, `${path}.maxFileBytes`, 1_024, 100_000_000, true);
470
+ }
471
+ if (!partial || value.maxContextTokens !== undefined) {
472
+ result.maxContextTokens = expectNumber(value.maxContextTokens, `${path}.maxContextTokens`, 1_000, 2_000_000, true);
473
+ }
474
+ return result;
475
+ }
476
+ function validateBudgets(raw, path, partial) {
477
+ const value = expectObject(raw, path);
478
+ assertKnownKeys(value, [
479
+ "maxCostUsd",
480
+ "maxInputTokens",
481
+ "maxOutputTokens",
482
+ "maxRequests",
483
+ "maxRepairs",
484
+ "timeoutMs",
485
+ ], path);
486
+ const result = {};
487
+ const set = (key, min, max, integer) => {
488
+ if (!partial || value[key] !== undefined) {
489
+ result[key] = expectNumber(value[key], `${path}.${key}`, min, max, integer);
490
+ }
491
+ };
492
+ set("maxCostUsd", 0, 100_000, false);
493
+ set("maxInputTokens", 1_000, 10_000_000, true);
494
+ set("maxOutputTokens", 1, 1_000_000, true);
495
+ set("maxRequests", 1, 100, true);
496
+ set("maxRepairs", 0, 2, true);
497
+ set("timeoutMs", 1_000, 86_400_000, true);
498
+ return result;
499
+ }
500
+ function validateSandbox(raw, path) {
501
+ const value = expectObject(raw, path);
502
+ assertKnownKeys(value, ["required", "engine", "network"], path);
503
+ const result = {};
504
+ if (value.required !== undefined) {
505
+ const required = expectBoolean(value.required, `${path}.required`);
506
+ if (!required) {
507
+ throw new ConfigError(`\`${path}.required\` must be true in schema v1; generated runs always require a strong sandbox.`);
508
+ }
509
+ result.required = true;
510
+ }
511
+ if (value.engine !== undefined) {
512
+ if (value.engine !== "auto" && value.engine !== "docker" && value.engine !== "podman") {
513
+ throw new ConfigError(`\`${path}.engine\` must be auto, docker, or podman.`);
514
+ }
515
+ result.engine = value.engine;
516
+ }
517
+ if (value.network !== undefined) {
518
+ if (value.network !== "none") {
519
+ throw new ConfigError(`\`${path}.network\` must be none in schema v1.`);
520
+ }
521
+ result.network = "none";
522
+ }
523
+ return result;
524
+ }
525
+ function validateWorkspace(raw, index) {
526
+ const path = `workspaces[${index}]`;
527
+ const value = expectObject(raw, path);
528
+ assertKnownKeys(value, ["root", "provider", "documentation", "privacy", "budgets"], path);
529
+ if (typeof value.root !== "string") {
530
+ throw new ConfigError(`\`${path}.root\` must be a string.`);
531
+ }
532
+ const result = {
533
+ root: validateRelativePath(value.root, `${path}.root`, true),
534
+ };
535
+ if (value.provider !== undefined)
536
+ result.provider = validateProvider(value.provider, `${path}.provider`);
537
+ if (value.documentation !== undefined) {
538
+ result.documentation = validateDocumentation(value.documentation, `${path}.documentation`, true);
539
+ }
540
+ if (value.privacy !== undefined) {
541
+ result.privacy = validatePrivacy(value.privacy, `${path}.privacy`, true);
542
+ }
543
+ if (value.budgets !== undefined) {
544
+ result.budgets = validateBudgets(value.budgets, `${path}.budgets`, true);
545
+ }
546
+ return result;
547
+ }
548
+ /** Validate an already-parsed schema-v1 object into a fully defaulted config. */
549
+ export function validateConfig(raw) {
550
+ if (!isObject(raw))
551
+ throw new ConfigError("Config root must be a JSON object.");
552
+ rejectCredentialKeys(raw);
553
+ assertKnownKeys(raw, [
554
+ "schemaVersion",
555
+ "language",
556
+ "filesToIgnore",
557
+ "allowNonHumanFiles",
558
+ "provider",
559
+ "workspaces",
560
+ "documentation",
561
+ "privacy",
562
+ "sandbox",
563
+ "budgets",
564
+ ], "");
565
+ if (raw.schemaVersion === undefined) {
566
+ throw new ConfigError(`Missing \`schemaVersion\`; run \`human-to-code migrate-config\` for alpha configuration.`);
567
+ }
568
+ if (raw.schemaVersion !== CONFIG_SCHEMA_VERSION) {
569
+ throw new ConfigError(`Unsupported \`schemaVersion\` ${JSON.stringify(raw.schemaVersion)}; expected ${CONFIG_SCHEMA_VERSION}.`);
570
+ }
571
+ const config = cloneDefaults();
572
+ if (raw.language !== undefined) {
573
+ if (typeof raw.language !== "string" ||
574
+ !LANGUAGES.includes(raw.language)) {
575
+ throw new ConfigError(`\`language\` must be one of: ${LANGUAGES.join(", ")}.`);
576
+ }
577
+ config.language = raw.language;
578
+ }
579
+ if (raw.filesToIgnore !== undefined) {
580
+ config.filesToIgnore = expectStringArray(raw.filesToIgnore, "filesToIgnore", validateIgnoreName);
581
+ }
582
+ if (raw.allowNonHumanFiles !== undefined) {
583
+ config.allowNonHumanFiles = expectBoolean(raw.allowNonHumanFiles, "allowNonHumanFiles");
584
+ }
585
+ if (raw.provider !== undefined)
586
+ config.provider = validateProvider(raw.provider);
587
+ if (raw.workspaces !== undefined) {
588
+ if (!Array.isArray(raw.workspaces)) {
589
+ throw new ConfigError("`workspaces` must be an array.");
590
+ }
591
+ config.workspaces = raw.workspaces.map(validateWorkspace);
592
+ const roots = config.workspaces.map(({ root }) => root.toLowerCase());
593
+ if (new Set(roots).size !== roots.length) {
594
+ throw new ConfigError("`workspaces` must not contain duplicate roots.");
595
+ }
596
+ }
597
+ if (raw.documentation !== undefined) {
598
+ config.documentation = {
599
+ ...config.documentation,
600
+ ...validateDocumentation(raw.documentation, "documentation", true),
601
+ };
602
+ }
603
+ if (raw.privacy !== undefined) {
604
+ config.privacy = {
605
+ ...config.privacy,
606
+ ...validatePrivacy(raw.privacy, "privacy", true),
607
+ };
608
+ }
609
+ if (raw.sandbox !== undefined) {
610
+ config.sandbox = {
611
+ ...config.sandbox,
612
+ ...validateSandbox(raw.sandbox, "sandbox"),
613
+ };
614
+ }
615
+ if (raw.budgets !== undefined) {
616
+ config.budgets = {
617
+ ...config.budgets,
618
+ ...validateBudgets(raw.budgets, "budgets", true),
619
+ };
620
+ }
621
+ return config;
622
+ }
623
+ /**
624
+ * Explicitly migrate the dependency-free 0.0.1 shape. It intentionally accepts
625
+ * only the four known legacy keys and then validates the resulting v1 object.
626
+ */
627
+ export function migrateLegacyConfig(raw) {
628
+ if (!isObject(raw))
629
+ throw new ConfigError("Legacy config root must be a JSON object.");
630
+ if (raw.schemaVersion !== undefined)
631
+ return validateConfig(raw);
632
+ rejectCredentialKeys(raw);
633
+ assertKnownKeys(raw, ["language", "filesToIgnore", "allowNonHumanFiles", "provider"], "");
634
+ return validateConfig({ schemaVersion: CONFIG_SCHEMA_VERSION, ...raw });
635
+ }
636
+ /**
637
+ * Load `<root>/human-to-code.config.json`. A missing file returns cloned
638
+ * defaults; unreadable, non-regular, symlinked, oversized, or legacy files fail.
639
+ */
640
+ export async function loadConfig(root) {
641
+ const path = join(root, CONFIG_FILENAME);
642
+ let metadata;
643
+ try {
644
+ metadata = await lstat(path);
645
+ }
646
+ catch (error) {
647
+ if (error.code === "ENOENT") {
648
+ return { config: cloneDefaults(), fromFile: false };
649
+ }
650
+ throw new ConfigError(`Could not inspect ${CONFIG_FILENAME}: ${String(error)}`);
651
+ }
652
+ if (metadata.isSymbolicLink() || !metadata.isFile()) {
653
+ throw new ConfigError(`${CONFIG_FILENAME} must be a regular, non-symlink file.`);
654
+ }
655
+ if (metadata.size > MAX_CONFIG_BYTES) {
656
+ throw new ConfigError(`${CONFIG_FILENAME} exceeds the ${MAX_CONFIG_BYTES}-byte limit.`);
657
+ }
658
+ let handle;
659
+ try {
660
+ // O_NOFOLLOW closes the lstat/open replacement race on platforms that
661
+ // support it; the descriptor identity comparison also detects replacement.
662
+ handle = await open(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
663
+ }
664
+ catch (error) {
665
+ throw new ConfigError(`Could not securely open ${CONFIG_FILENAME}: ${String(error)}`);
666
+ }
667
+ let text;
668
+ try {
669
+ const openedMetadata = await handle.stat();
670
+ if (!openedMetadata.isFile() ||
671
+ openedMetadata.dev !== metadata.dev ||
672
+ openedMetadata.ino !== metadata.ino) {
673
+ throw new ConfigError(`${CONFIG_FILENAME} changed while it was being opened.`);
674
+ }
675
+ if (openedMetadata.size > MAX_CONFIG_BYTES) {
676
+ throw new ConfigError(`${CONFIG_FILENAME} exceeds the ${MAX_CONFIG_BYTES}-byte limit.`);
677
+ }
678
+ text = await handle.readFile("utf8");
679
+ if (Buffer.byteLength(text, "utf8") > MAX_CONFIG_BYTES) {
680
+ throw new ConfigError(`${CONFIG_FILENAME} exceeds the ${MAX_CONFIG_BYTES}-byte limit.`);
681
+ }
682
+ }
683
+ catch (error) {
684
+ if (error instanceof ConfigError)
685
+ throw error;
686
+ throw new ConfigError(`Could not read ${CONFIG_FILENAME}: ${String(error)}`);
687
+ }
688
+ finally {
689
+ await handle.close().catch(() => undefined);
690
+ }
691
+ let parsed;
692
+ try {
693
+ parsed = JSON.parse(text);
694
+ }
695
+ catch (error) {
696
+ throw new ConfigError(`${CONFIG_FILENAME} is not valid JSON: ${String(error)}`);
697
+ }
698
+ return { config: validateConfig(parsed), fromFile: true };
699
+ }
700
+ /** Serialize a fresh schema-v1 default config for initialization. */
701
+ export function defaultConfigJson() {
702
+ return `${JSON.stringify(DEFAULT_CONFIG, null, 2)}\n`;
703
+ }
704
+ //# sourceMappingURL=config.js.map