celery-env 0.1.3 → 0.1.4

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/README.md CHANGED
@@ -171,7 +171,7 @@ competitors were checked with `npm view` on 2026-06-25.
171
171
 
172
172
  | Package | Version checked | Runtime deps | Unpacked npm size | Files |
173
173
  | --- | ---: | ---: | ---: | ---: |
174
- | `celery-env` | 0.1.2 + infer | 0 | 117.7 kB | 26 |
174
+ | `celery-env` | 0.1.4 | 0 | 119.2 kB | 26 |
175
175
  | `zod` | 4.4.3 | 0 | 4.56 MB | 718 |
176
176
  | `valibot` | 1.4.1 | 0 | 1.84 MB | 9 |
177
177
  | `envalid` | 8.2.0 | 1 | 88.8 kB | 39 |
@@ -73,7 +73,7 @@ Package footprint is separate from runtime speed. Celery is this branch's
73
73
 
74
74
  | Package | Version Checked | Runtime Deps | Unpacked npm Size | Files |
75
75
  | --- | ---: | ---: | ---: | ---: |
76
- | `celery-env` | 0.1.2 + infer | 0 | 117.7 kB | 26 |
76
+ | `celery-env` | 0.1.4 | 0 | 119.2 kB | 26 |
77
77
  | `zod` | 4.4.3 | 0 | 4.56 MB | 718 |
78
78
  | `valibot` | 1.4.1 | 0 | 1.84 MB | 9 |
79
79
  | `envalid` | 8.2.0 | 1 | 88.8 kB | 39 |
package/docs/CLI.md CHANGED
@@ -42,6 +42,7 @@ npx celery-env infer \
42
42
  Inference is conservative. Ambiguous values become `str({ min: 1 })`. Only
43
43
  example, sample, or template env files can emit `example` metadata; local env
44
44
  values and secret-looking values are not copied into the generated schema.
45
+ Safe example values can infer enums and string-list item enums.
45
46
  Review the result for project-specific constraints such as `requiredWhen`,
46
47
  `min`, `max`, or stricter URL protocols.
47
48
 
@@ -94,7 +94,7 @@ This table is package metadata, not benchmark speed. Celery is this branch's
94
94
 
95
95
  | Package | Version Checked | Runtime Deps | Unpacked npm Size | Files |
96
96
  | --- | ---: | ---: | ---: | ---: |
97
- | `celery-env` | 0.1.2 + infer | 0 | 117.7 kB | 26 |
97
+ | `celery-env` | 0.1.4 | 0 | 119.2 kB | 26 |
98
98
  | `zod` | 4.4.3 | 0 | 4.56 MB | 718 |
99
99
  | `valibot` | 1.4.1 | 0 | 1.84 MB | 9 |
100
100
  | `envalid` | 8.2.0 | 1 | 88.8 kB | 39 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "celery-env",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Type-safe process.env validation that generates zero-dependency standalone validators.",
5
5
  "type": "module",
6
6
  "types": "./src/index.d.ts",
package/src/infer.js CHANGED
@@ -10,6 +10,7 @@ const DEFAULT_SCAN_PATHS = ["src", "app", "pages", "lib", "server"];
10
10
  const BOOL_VALUES = new Set(["true", "yes", "on", "false", "no", "off"]);
11
11
  const SECRET_KEY = /(?:SECRET|TOKEN|PASSWORD|PASS|PRIVATE|CREDENTIAL|AUTH|API_KEY|ACCESS_KEY)/i;
12
12
  const SECRET_VALUE = /(?:^sk_|^pk_|^gh[pousr]_|^xox[baprs]-|^eyJ|-----BEGIN |:\/\/[^/\s:@]+:[^/\s:@]+@|[A-Za-z0-9+/=_-]{32,})/;
13
+ const ENUM_VALUE = /^[A-Za-z][A-Za-z0-9_.:-]{0,31}$/;
13
14
  const MAX_ENV_FILE_BYTES = 256 * 1024;
14
15
  const MAX_SOURCE_FILE_BYTES = 1024 * 1024;
15
16
  const MAX_SOURCE_FILES = 2000;
@@ -256,28 +257,29 @@ function inferRule(entry) {
256
257
  return { kind: "oneOf", values: ["development", "test", "production"], options: { default: "development" } };
257
258
  }
258
259
 
259
- const samples = entry.values.map((item) => item.value).filter((value) => value !== "");
260
+ const samples = entry.values.filter((item) => item.value !== "");
260
261
  if (!samples.length) return { kind: "str", options: { min: 1 } };
261
262
 
262
- const kinds = samples.map(inferValue);
263
+ const enumRule = sampleEnumRule(entry, samples);
264
+ if (enumRule) return withExample(entry, enumRule);
265
+
266
+ const kinds = samples.map((item) => inferValue(item.value, enumSafe(entry, item)));
263
267
  if (kinds.some((kind) => kind.kind === "str")) return stringRule(entry);
264
268
 
265
269
  const first = kinds[0].kind;
266
270
  if (!kinds.every((kind) => kind.kind === first)) return stringRule(entry);
267
271
 
268
272
  const rule = mergeRules(first, kinds);
269
- const example = safeExample(entry, rule);
270
- if (example !== undefined) rule.options = { ...rule.options, example };
271
- return rule;
273
+ return withExample(entry, rule);
272
274
  }
273
275
 
274
- function inferValue(value) {
276
+ function inferValue(value, allowEnum) {
275
277
  if (BOOL_VALUES.has(value)) return { kind: "bool" };
276
278
  if (strictInt(value)) return { kind: "int", options: { strict: true } };
277
279
  if (strictNumber(value)) return { kind: "num", options: { strict: true } };
278
280
  const jsonRule = inferJson(value);
279
281
  if (jsonRule) return jsonRule;
280
- const listRule = inferList(value);
282
+ const listRule = inferList(value, allowEnum);
281
283
  if (listRule) return listRule;
282
284
  const urlRule = inferUrl(value);
283
285
  if (urlRule) return urlRule;
@@ -292,7 +294,7 @@ function inferJson(value) {
292
294
  } catch {}
293
295
  }
294
296
 
295
- function inferList(value) {
297
+ function inferList(value, allowEnum) {
296
298
  if (!value.includes(",") || /^\s*[\[{]/.test(value)) return;
297
299
  const parts = value.split(",").map((part) => part.trim());
298
300
  if (parts.length < 2 || parts.some((part) => part === "")) return;
@@ -304,7 +306,11 @@ function inferList(value) {
304
306
  return urlRule || { kind: "str" };
305
307
  });
306
308
  const first = items[0].kind;
307
- if (first === "str" || !items.every((item) => item.kind === first)) return;
309
+ if (first === "str") {
310
+ const values = allowEnum && enumValues(parts);
311
+ return { kind: "list", item: values ? { kind: "oneOf", values } : { kind: "str", options: { min: 1 } } };
312
+ }
313
+ if (!items.every((item) => item.kind === first)) return;
308
314
  return { kind: "list", item: mergeRules(first, items) };
309
315
  }
310
316
 
@@ -334,9 +340,12 @@ function mergeRules(kind, rules) {
334
340
  }
335
341
  if (kind === "list") {
336
342
  const itemKind = rules[0].item.kind;
343
+ if (!rules.every((rule) => rule.item.kind === itemKind)) return { kind: "list", item: { kind: "str", options: { min: 1 } } };
337
344
  return { kind: "list", item: mergeRules(itemKind, rules.map((rule) => rule.item)) };
338
345
  }
346
+ if (kind === "oneOf") return { kind, values: enumValues(rules.flatMap((rule) => rule.values)) };
339
347
  if (kind === "int" || kind === "num") return { kind, options: { strict: true } };
348
+ if (kind === "str" && rules.some((rule) => rule.options)) return { kind, options: { min: 1 } };
340
349
  return { kind };
341
350
  }
342
351
 
@@ -349,10 +358,29 @@ function stringRule(entry) {
349
358
 
350
359
  function safeExample(entry, rule) {
351
360
  const sample = entry.values.find((item) => item.safeExamples && item.value !== "");
352
- if (!sample || SECRET_KEY.test(entry.key) || SECRET_VALUE.test(sample.value)) return;
361
+ if (!sample || unsafe(entry.key, sample.value)) return;
353
362
  return exampleValue(sample.value, rule);
354
363
  }
355
364
 
365
+ function withExample(entry, rule) {
366
+ const example = safeExample(entry, rule);
367
+ if (example !== undefined) rule.options = { ...rule.options, example };
368
+ return rule;
369
+ }
370
+
371
+ function enumSafe(entry, item) {
372
+ return item.safeExamples && !unsafe(entry.key, item.value);
373
+ }
374
+
375
+ function sampleEnumRule(entry, samples) {
376
+ const values = samples.every((item) => enumSafe(entry, item)) && enumValues(samples.map((item) => item.value));
377
+ if (values && values.length > 1 && values.length <= 8) return { kind: "oneOf", values };
378
+ }
379
+
380
+ function unsafe(key, value) {
381
+ return SECRET_KEY.test(key) || SECRET_VALUE.test(value);
382
+ }
383
+
356
384
  function exampleValue(value, rule) {
357
385
  if (rule.kind === "bool") return value === "true" || value === "1" || value === "yes" || value === "on";
358
386
  if (rule.kind === "int" || rule.kind === "num") return Number(value);
@@ -373,7 +401,7 @@ function collectRuleImports(imports, rule) {
373
401
  }
374
402
 
375
403
  function ruleSource(rule) {
376
- if (rule.kind === "oneOf") return `oneOf(${literal(rule.values)}, ${literal(rule.options)})`;
404
+ if (rule.kind === "oneOf") return rule.options && Object.keys(rule.options).length ? `oneOf(${literal(rule.values)}, ${literal(rule.options)})` : `oneOf(${literal(rule.values)})`;
377
405
  if (rule.kind === "list") {
378
406
  const item = ruleSource(rule.item);
379
407
  return rule.options && Object.keys(rule.options).length ? `list(${item}, ${literal(rule.options)})` : `list(${item})`;
@@ -390,6 +418,11 @@ function literal(value) {
390
418
  return JSON.stringify(value);
391
419
  }
392
420
 
421
+ function enumValues(values) {
422
+ if (!values.every((value) => ENUM_VALUE.test(value))) return;
423
+ return [...new Set(values)].sort();
424
+ }
425
+
393
426
  function importSort(a, b) {
394
427
  return a.localeCompare(b);
395
428
  }