@wchen.ai/env-from-example 1.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.
@@ -0,0 +1,236 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import { getRootDirFromArgv, parseEnvExample, getExistingEnvVersion, getExistingEnvVariables, serializeEnvExample, polishEnvExample, bumpSemver, updateEnvSchemaVersion, } from '../../setup-env.js';
5
+ const FIXTURES_DIR = path.resolve(__dirname, '../fixtures');
6
+ describe('getRootDirFromArgv', () => {
7
+ const originalArgv = process.argv;
8
+ afterEach(() => {
9
+ process.argv = originalArgv;
10
+ });
11
+ it('returns process.cwd() when --cwd is not present', () => {
12
+ process.argv = ['node', 'setup-env.js'];
13
+ expect(getRootDirFromArgv()).toBe(process.cwd());
14
+ });
15
+ it('returns resolved path when --cwd is present with value', () => {
16
+ process.argv = ['node', 'setup-env.js', '--cwd', '/some/project'];
17
+ expect(getRootDirFromArgv()).toBe(path.resolve('/some/project'));
18
+ });
19
+ it('returns process.cwd() when --cwd is last (no value)', () => {
20
+ process.argv = ['node', 'setup-env.js', '--yes', '--cwd'];
21
+ expect(getRootDirFromArgv()).toBe(process.cwd());
22
+ });
23
+ it('returns resolved path for relative --cwd', () => {
24
+ process.argv = ['node', 'setup-env.js', '--cwd', './fixtures/full'];
25
+ const result = getRootDirFromArgv();
26
+ expect(path.isAbsolute(result)).toBe(true);
27
+ expect(result).toMatch(/fixtures[\\/]full$/);
28
+ });
29
+ });
30
+ describe('parseEnvExample', () => {
31
+ it('throws when .env.example does not exist', () => {
32
+ expect(() => parseEnvExample('/nonexistent/dir')).toThrow(/.env.example not found at/);
33
+ });
34
+ it('parses full fixture: version, sections, required, commented-out', () => {
35
+ const rootDir = path.join(FIXTURES_DIR, 'full');
36
+ const { version, variables } = parseEnvExample(rootDir);
37
+ expect(version).toBe('1.0');
38
+ const keys = variables.map((v) => v.key);
39
+ expect(keys).toContain('DATABASE_URL');
40
+ expect(keys).toContain('DATABASE_POOL_SIZE');
41
+ expect(keys).toContain('API_KEY');
42
+ expect(keys).toContain('API_SECRET');
43
+ expect(keys).toContain('API_BASE_URL');
44
+ expect(keys).toContain('NODE_ENV');
45
+ expect(keys).toContain('SESSION_SECRET');
46
+ expect(keys).toContain('FEATURE_BETA');
47
+ expect(keys).toContain('PORT');
48
+ const databaseUrl = variables.find((v) => v.key === 'DATABASE_URL');
49
+ expect(databaseUrl.defaultValue).toBe('postgres://localhost:5432/myapp');
50
+ expect(databaseUrl.required).toBe(true);
51
+ expect(databaseUrl.isCommentedOut).toBe(false);
52
+ expect(databaseUrl.comment).toMatch(/Postgres|REQUIRED/);
53
+ const apiBaseUrl = variables.find((v) => v.key === 'API_BASE_URL');
54
+ expect(apiBaseUrl.defaultValue).toBe('https://api.example.com/v1');
55
+ const featureBeta = variables.find((v) => v.key === 'FEATURE_BETA');
56
+ expect(featureBeta.isCommentedOut).toBe(true);
57
+ expect(featureBeta.defaultValue).toBe('false');
58
+ const port = variables.find((v) => v.key === 'PORT');
59
+ expect(port.isCommentedOut).toBe(true);
60
+ expect(port.defaultValue).toBe('3000');
61
+ });
62
+ it('parses minimal fixture with version', () => {
63
+ const rootDir = path.join(FIXTURES_DIR, 'minimal');
64
+ const { version, variables } = parseEnvExample(rootDir);
65
+ expect(version).toBe('2.0');
66
+ expect(variables).toHaveLength(2);
67
+ const nodeEnv = variables.find((v) => v.key === 'NODE_ENV');
68
+ expect(nodeEnv.defaultValue).toBe('development');
69
+ expect(nodeEnv.required).toBe(false);
70
+ const someKey = variables.find((v) => v.key === 'SOME_KEY');
71
+ expect(someKey.defaultValue).toBe('default_value');
72
+ });
73
+ it('parses required-only fixture', () => {
74
+ const rootDir = path.join(FIXTURES_DIR, 'required-only');
75
+ const { version, variables } = parseEnvExample(rootDir);
76
+ expect(version).toBe('1.0');
77
+ expect(variables).toHaveLength(1);
78
+ expect(variables[0].key).toBe('REQUIRED_VAR');
79
+ expect(variables[0].required).toBe(true);
80
+ expect(variables[0].defaultValue).toBe('');
81
+ });
82
+ it('parses no-version fixture: version is null', () => {
83
+ const rootDir = path.join(FIXTURES_DIR, 'no-version');
84
+ const { version, variables } = parseEnvExample(rootDir);
85
+ expect(version).toBeNull();
86
+ expect(variables).toHaveLength(2);
87
+ expect(variables.find((v) => v.key === 'FOO')?.defaultValue).toBe('bar');
88
+ expect(variables.find((v) => v.key === 'BAZ')?.defaultValue).toBe('qux');
89
+ });
90
+ it('strips inline comments from values', () => {
91
+ const rootDir = path.join(FIXTURES_DIR, 'full');
92
+ const { variables } = parseEnvExample(rootDir);
93
+ // API_BASE_URL has no inline comment in fixture; DATABASE_URL might have comment in some examples
94
+ // Check that quoted values are unquoted
95
+ const apiBase = variables.find((v) => v.key === 'API_BASE_URL');
96
+ expect(apiBase.defaultValue).toBe('https://api.example.com/v1');
97
+ });
98
+ it('preserves section headers in comment', () => {
99
+ const rootDir = path.join(FIXTURES_DIR, 'full');
100
+ const { variables } = parseEnvExample(rootDir);
101
+ const dbUrl = variables.find((v) => v.key === 'DATABASE_URL');
102
+ expect(dbUrl.comment).toMatch(/------/);
103
+ });
104
+ });
105
+ describe('getExistingEnvVersion', () => {
106
+ it('returns null for content without version', () => {
107
+ expect(getExistingEnvVersion('FOO=bar')).toBeNull();
108
+ expect(getExistingEnvVersion('')).toBeNull();
109
+ });
110
+ it('returns version from quoted ENV_SCHEMA_VERSION', () => {
111
+ const content = '# ENV_SCHEMA_VERSION="1.0"\nFOO=bar';
112
+ expect(getExistingEnvVersion(content)).toBe('1.0');
113
+ });
114
+ it('returns version from unquoted ENV_SCHEMA_VERSION', () => {
115
+ const content = '# ENV_SCHEMA_VERSION=2.0\n';
116
+ expect(getExistingEnvVersion(content)).toBe('2.0');
117
+ });
118
+ it('returns first match when multiple version-like lines exist', () => {
119
+ const content = '# ENV_SCHEMA_VERSION="1.0"\n# ENV_SCHEMA_VERSION="2.0"';
120
+ expect(getExistingEnvVersion(content)).toBe('1.0');
121
+ });
122
+ });
123
+ describe('getExistingEnvVariables', () => {
124
+ it('returns empty object when file does not exist', () => {
125
+ const result = getExistingEnvVariables(path.join(FIXTURES_DIR, 'nonexistent.env'));
126
+ expect(result).toEqual({});
127
+ });
128
+ it('parses existing .env file', () => {
129
+ const envPath = path.join(FIXTURES_DIR, 'full', '.env.example');
130
+ const result = getExistingEnvVariables(envPath);
131
+ expect(result).toBeDefined();
132
+ expect(typeof result).toBe('object');
133
+ expect(result.DATABASE_URL).toBe('postgres://localhost:5432/myapp');
134
+ expect(result.NODE_ENV).toBe('development');
135
+ });
136
+ it('returns empty object for empty or comment-only file', () => {
137
+ const tmpDir = path.join(FIXTURES_DIR, 'full');
138
+ const commentOnlyPath = path.join(tmpDir, '.env.comment-only');
139
+ fs.writeFileSync(commentOnlyPath, '# only comments\n\n', 'utf-8');
140
+ try {
141
+ const result = getExistingEnvVariables(commentOnlyPath);
142
+ expect(result).toEqual({});
143
+ }
144
+ finally {
145
+ try {
146
+ fs.unlinkSync(commentOnlyPath);
147
+ }
148
+ catch {
149
+ // ignore
150
+ }
151
+ }
152
+ });
153
+ });
154
+ describe('serializeEnvExample', () => {
155
+ it('outputs version line and variables with sections', () => {
156
+ const variables = [
157
+ {
158
+ key: 'FOO',
159
+ defaultValue: 'bar',
160
+ comment: '# ------ Section ------\nDescription',
161
+ required: false,
162
+ isCommentedOut: false,
163
+ },
164
+ {
165
+ key: 'BAZ',
166
+ defaultValue: 'qux',
167
+ comment: '',
168
+ required: false,
169
+ isCommentedOut: true,
170
+ },
171
+ ];
172
+ const out = serializeEnvExample('1.0', variables);
173
+ expect(out).toMatch(/# ENV_SCHEMA_VERSION="1.0"/);
174
+ expect(out).toMatch(/# ------ Section ------/);
175
+ expect(out).toMatch(/FOO=bar/);
176
+ expect(out).toMatch(/# BAZ=qux/);
177
+ });
178
+ it('outputs no version line when version is null', () => {
179
+ const variables = [
180
+ { key: 'X', defaultValue: 'y', comment: '', required: false, isCommentedOut: false },
181
+ ];
182
+ const out = serializeEnvExample(null, variables);
183
+ expect(out).not.toMatch(/ENV_SCHEMA_VERSION/);
184
+ expect(out).toMatch(/^X=y/);
185
+ });
186
+ });
187
+ describe('bumpSemver', () => {
188
+ it('bumps patch', () => {
189
+ expect(bumpSemver('1.0.0', 'patch')).toBe('1.0.1');
190
+ expect(bumpSemver('1.0', 'patch')).toBe('1.0.1');
191
+ });
192
+ it('bumps minor', () => {
193
+ expect(bumpSemver('1.0.0', 'minor')).toBe('1.1.0');
194
+ expect(bumpSemver('2.1', 'minor')).toBe('2.2.0');
195
+ });
196
+ it('bumps major', () => {
197
+ expect(bumpSemver('1.0.0', 'major')).toBe('2.0.0');
198
+ expect(bumpSemver('3.2.1', 'major')).toBe('4.0.0');
199
+ });
200
+ });
201
+ describe('polishEnvExample', () => {
202
+ it('overwrites .env.example with normalized content and dedupes keys', () => {
203
+ const fixtureDir = path.join(FIXTURES_DIR, 'full');
204
+ const envPath = path.join(fixtureDir, '.env.example');
205
+ const before = fs.readFileSync(envPath, 'utf-8');
206
+ polishEnvExample(fixtureDir);
207
+ const after = fs.readFileSync(envPath, 'utf-8');
208
+ expect(after).toMatch(/# ENV_SCHEMA_VERSION="1.0"/);
209
+ expect(after).toMatch(/DATABASE_URL=postgres:\/\/localhost:5432\/myapp/);
210
+ expect(after).toMatch(/# ------ Database ------/);
211
+ // Restore original so other tests and fixtures are unchanged
212
+ fs.writeFileSync(envPath, before, 'utf-8');
213
+ });
214
+ it('throws when .env.example does not exist', () => {
215
+ expect(() => polishEnvExample('/nonexistent/dir')).toThrow(/.env.example not found/);
216
+ });
217
+ });
218
+ describe('updateEnvSchemaVersion', () => {
219
+ it('updates ENV_SCHEMA_VERSION in .env.example', () => {
220
+ const fixtureDir = path.join(FIXTURES_DIR, 'minimal');
221
+ const envPath = path.join(fixtureDir, '.env.example');
222
+ const before = fs.readFileSync(envPath, 'utf-8');
223
+ try {
224
+ updateEnvSchemaVersion(fixtureDir, '3.0.0');
225
+ const after = fs.readFileSync(envPath, 'utf-8');
226
+ expect(after).toMatch(/# ENV_SCHEMA_VERSION="3.0.0"/);
227
+ expect(after).toMatch(/NODE_ENV=development/);
228
+ }
229
+ finally {
230
+ fs.writeFileSync(envPath, before, 'utf-8');
231
+ }
232
+ });
233
+ it('throws when .env.example does not exist', () => {
234
+ expect(() => updateEnvSchemaVersion('/nonexistent', '1.0.0')).toThrow(/.env.example not found/);
235
+ });
236
+ });
@@ -0,0 +1,15 @@
1
+ import { defineConfig } from "vitest/config";
2
+ import { resolve } from "path";
3
+ export default defineConfig({
4
+ test: {
5
+ include: ["test/**/*.test.ts"],
6
+ environment: "node",
7
+ globals: true,
8
+ fileParallelism: false,
9
+ },
10
+ resolve: {
11
+ alias: {
12
+ "env-from-example": resolve(__dirname, "env-from-example.ts"),
13
+ },
14
+ },
15
+ });
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@wchen.ai/env-from-example",
3
+ "version": "1.0.0",
4
+ "description": "Interactive and non-interactive CLI to set up .env from .env.example",
5
+ "keywords": [
6
+ "env",
7
+ "dotenv",
8
+ "cli",
9
+ "environment",
10
+ "setup"
11
+ ],
12
+ "homepage": "https://github.com/wchen02/env-from-example#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/wchen02/env-from-example/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/wchen02/env-from-example.git"
19
+ },
20
+ "license": "ISC",
21
+ "author": "Wensheng Chen",
22
+ "type": "module",
23
+ "main": "dist/env-from-example.js",
24
+ "bin": {
25
+ "env-from-example": "dist/env-from-example.js"
26
+ },
27
+ "directories": {
28
+ "test": "test"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "schema.json"
33
+ ],
34
+ "scripts": {
35
+ "build": "tsc",
36
+ "prepublishOnly": "pnpm run build",
37
+ "start": "node dist/env-from-example.js",
38
+ "dev": "tsx env-from-example.ts",
39
+ "test": "vitest run",
40
+ "test:watch": "vitest",
41
+ "lint": "eslint .",
42
+ "format": "prettier --write .",
43
+ "format:check": "prettier --check .",
44
+ "typecheck": "tsc -p tsconfig.typecheck.json",
45
+ "code-quality": "pnpm run lint && pnpm run format:check && pnpm run typecheck"
46
+ },
47
+ "dependencies": {
48
+ "@inquirer/prompts": "^5.4.0",
49
+ "commander": "^12.1.0",
50
+ "dotenv": "^16.4.5",
51
+ "picocolors": "^1.1.0"
52
+ },
53
+ "devDependencies": {
54
+ "@eslint/js": "^9.15.0",
55
+ "@types/node": "^22.10.0",
56
+ "eslint": "^9.15.0",
57
+ "eslint-config-prettier": "^9.1.0",
58
+ "prettier": "^3.4.0",
59
+ "tsx": "^4.19.0",
60
+ "typescript": "^5.7.0",
61
+ "typescript-eslint": "^8.15.0",
62
+ "vitest": "^2.1.0"
63
+ },
64
+ "packageManager": "pnpm@10.30.0"
65
+ }
package/schema.json ADDED
@@ -0,0 +1,263 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://schemas.example.com/env-types-priority-array-pruned.schema.json",
4
+ "title": "Environment Variable Runtime Types (Priority Ordered, pruned)",
5
+ "description": "Pruned ordered env value type definitions for deterministic value→type detection. Ambiguous types removed so detection falls back to primitives when ambiguous.",
6
+ "type": "object",
7
+ "types": [
8
+ {
9
+ "name": "credentials/private_key_pem",
10
+ "type": "string",
11
+ "pattern": "^-----BEGIN [A-Z ]+KEY-----[\\s\\S]*-----END [A-Z ]+KEY-----$",
12
+ "description": "PEM private key (-----BEGIN ... KEY-----).",
13
+ "examples": [
14
+ "-----BEGIN PRIVATE KEY-----\\nMIIEvQIBADANBgkq...\\n-----END PRIVATE KEY-----"
15
+ ],
16
+ "auto_generate": "rsa_private_key"
17
+ },
18
+
19
+ {
20
+ "name": "network/https_url",
21
+ "type": "string",
22
+ "pattern": "^https:\\/\\/[^\\s]+$",
23
+ "description": "HTTPS URL.",
24
+ "examples": [
25
+ "https://api.example.com",
26
+ "https://hooks.example.com/ingest"
27
+ ],
28
+ "default": "https://localhost:443"
29
+ },
30
+
31
+ {
32
+ "name": "network/url",
33
+ "type": "string",
34
+ "pattern": "^https?:\\/\\/[^\\s]+$",
35
+ "description": "HTTP or HTTPS URL.",
36
+ "examples": ["http://localhost:3000", "https://api.example.com/v1"],
37
+ "default": "http://localhost:80"
38
+ },
39
+
40
+ {
41
+ "name": "network/uri",
42
+ "type": "string",
43
+ "pattern": "^(?!https?:\\/\\/)[a-zA-Z][a-zA-Z0-9+.\\-]*:\\/\\/.+$",
44
+ "description": "Non-HTTP service URI (e.g. postgres://).",
45
+ "examples": [
46
+ "postgres://user:pass@db:5432/mydb?sslmode=require",
47
+ "redis://:password@redis:6379/0",
48
+ "s3://my-bucket/path"
49
+ ]
50
+ },
51
+
52
+ {
53
+ "name": "network/domain",
54
+ "type": "string",
55
+ "pattern": "^[a-zA-Z0-9\\-]+(\\.[a-zA-Z0-9\\-]+)*\\.[a-zA-Z]{2,}$",
56
+ "description": "Domain or hostname with TLD (e.g. example.com).",
57
+ "examples": ["example.com", "api.bestpos.io"]
58
+ },
59
+
60
+ {
61
+ "name": "network/ip",
62
+ "type": "string",
63
+ "pattern": "^(25[0-5]|2[0-4]\\d|1?\\d{1,2})(\\.(25[0-5]|2[0-4]\\d|1?\\d{1,2})){3}$",
64
+ "description": "IPv4 dotted-decimal address (e.g. 127.0.0.1).",
65
+ "examples": ["127.0.0.1", "10.0.0.5"],
66
+ "default": "127.0.0.1"
67
+ },
68
+
69
+ {
70
+ "name": "network/ipv6",
71
+ "type": "string",
72
+ "pattern": "^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^(?:[0-9a-fA-F]{1,4}:){1,7}:$|^:(:[0-9a-fA-F]{1,4}){1,7}$|^(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}$",
73
+ "description": "IPv6 address (compressed or full notation).",
74
+ "examples": ["::1", "fe80::1", "2001:db8::1"],
75
+ "default": "::1"
76
+ },
77
+
78
+ {
79
+ "name": "version/semver",
80
+ "type": "string",
81
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$",
82
+ "description": "Semver: MAJOR.MINOR.PATCH + optional suffix.",
83
+ "examples": ["1.2.3", "0.10.0-alpha.1"],
84
+ "default": "0.0.0"
85
+ },
86
+
87
+ {
88
+ "name": "id/uuid",
89
+ "type": "string",
90
+ "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$",
91
+ "description": "UUID in canonical form (v1–v5).",
92
+ "examples": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"],
93
+ "auto_generate": "uuidv4"
94
+ },
95
+
96
+ {
97
+ "name": "structured/json",
98
+ "type": "string",
99
+ "pattern": "^\\{[\\s\\S]*\\}$",
100
+ "description": "JSON object as string; validate with parse.",
101
+ "examples": ["{\"ai\":true}", "{\"features\":[\"a\",\"b\"]}"],
102
+ "default": "{}"
103
+ },
104
+
105
+ {
106
+ "name": "structured/key_value_pairs",
107
+ "type": "string",
108
+ "pattern": "^[^=,]+=.+(,[^=,]+=.+)*$",
109
+ "description": "Comma-separated key=value pairs.",
110
+ "examples": ["env=prod,region=us-east-1", "tier=premium,plan=annual"]
111
+ },
112
+
113
+ {
114
+ "name": "locale/langtag",
115
+ "type": "string",
116
+ "pattern": "^[a-zA-Z]{2,3}(?:-[a-zA-Z]{2,4})?(?:-[A-Za-z0-9-]+)*$",
117
+ "description": "BCP-47 language tag (e.g. en, en-US).",
118
+ "examples": ["en", "en-US", "zh-Hant-TW"],
119
+ "default": "en-US"
120
+ },
121
+
122
+ {
123
+ "name": "structured/email_list",
124
+ "type": "string",
125
+ "pattern": "^[^\\s,@]+@[^\\s,@]+\\.[^\\s,@]+(?:,[^\\s,@]+@[^\\s,@]+\\.[^\\s,@]+)+$",
126
+ "description": "Comma-separated email list; validate each.",
127
+ "examples": ["a@example.com,b@x.org"]
128
+ },
129
+
130
+ {
131
+ "name": "structured/csv",
132
+ "type": "string",
133
+ "pattern": "^[^,]+(,[^,]+)+$",
134
+ "description": "Comma-separated list (2+ values).",
135
+ "examples": ["a,b,c", "one,two"]
136
+ },
137
+
138
+ {
139
+ "name": "file/windows_path",
140
+ "type": "string",
141
+ "pattern": "^[a-zA-Z]:\\\\(?:[^<>:\\\"/\\\\|?*\\r\\n]+\\\\)*[^<>:\\\"/\\\\|?*\\r\\n]*$",
142
+ "description": "Windows absolute path (e.g. C:\\dir\\file).",
143
+ "examples": ["C:\\Program Files\\App\\config.yml", "C:\\temp\\file.txt"]
144
+ },
145
+
146
+ {
147
+ "name": "file/path",
148
+ "type": "string",
149
+ "pattern": "^(?!\\s*$)(\\.?\\.?\\/)?([^\\/\\0]+\\/)*[^\\/\\0]+$",
150
+ "description": "POSIX path (relative or absolute).",
151
+ "examples": [
152
+ "./config.yml",
153
+ "/var/app/secrets.json",
154
+ "secrets/gcp/key.json"
155
+ ]
156
+ },
157
+
158
+ {
159
+ "name": "temporal/cron",
160
+ "type": "string",
161
+ "pattern": "^([\\d\\*/,-]+\\s){4}[\\d\\*/,-]+$",
162
+ "description": "Five-field cron (min hour day month dow).",
163
+ "examples": ["0 3 1 * *", "*/15 * * * *"],
164
+ "default": "0 0 * * *"
165
+ },
166
+
167
+ {
168
+ "name": "temporal/time_hhmm",
169
+ "type": "string",
170
+ "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
171
+ "description": "Time in 24-hour format (HH:MM).",
172
+ "examples": ["03:00", "23:59"],
173
+ "default": "00:00"
174
+ },
175
+
176
+ {
177
+ "name": "temporal/duration",
178
+ "type": "string",
179
+ "pattern": "^\\d+(ms|s|m|h|d)$",
180
+ "description": "Duration with unit (e.g. 30s, 5m, 1h, 7d).",
181
+ "examples": ["30s", "7d", "1h"],
182
+ "default": "1h"
183
+ },
184
+
185
+ {
186
+ "name": "credentials/secret",
187
+ "type": "string",
188
+ "minLength": 16,
189
+ "description": "API key, token, or secret (min length).",
190
+ "examples": ["sk_live_4f3b2a1c...", "superlongrandomsecretvalue"],
191
+ "auto_generate": "random_secret_32",
192
+ "constraints": {
193
+ "minLength": "value.length >= minLength",
194
+ "maxLength": "value.length <= maxLength"
195
+ }
196
+ },
197
+
198
+ {
199
+ "name": "visual/hex_color",
200
+ "type": "string",
201
+ "pattern": "^#(?:[A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$",
202
+ "description": "Hex color (#RGB, #RRGGBB, #RRGGBBAA).",
203
+ "examples": ["#fff", "#aabbcc", "#AABBCCFF"],
204
+ "default": "#ffffff"
205
+ },
206
+
207
+ {
208
+ "name": "structured/enum",
209
+ "type": "string",
210
+ "pattern": "^([a-zA-Z\\-_]*)$",
211
+ "description": "Value from a predefined set of options.",
212
+ "examples": ["debug", "info", "production"],
213
+ "constraints": {
214
+ "pattern": "new RegExp(pattern).test(value) // pattern should be ^(opt1|opt2|...)$ with valid options"
215
+ }
216
+ },
217
+
218
+ {
219
+ "name": "float",
220
+ "type": "number",
221
+ "description": "Floating-point number.",
222
+ "examples": [0.0, 3.1415],
223
+ "constraints": {
224
+ "min": "value >= min",
225
+ "max": "value <= max",
226
+ "precision": "limit decimal places (e.g. toFixed or check rounded equality)"
227
+ },
228
+ "default": 0.0
229
+ },
230
+
231
+ {
232
+ "name": "integer",
233
+ "type": "integer",
234
+ "description": "Integer.",
235
+ "examples": [0, 1, 42],
236
+ "constraints": {
237
+ "min": "value >= min",
238
+ "max": "value <= max"
239
+ },
240
+ "default": 0
241
+ },
242
+
243
+ {
244
+ "name": "boolean",
245
+ "type": "boolean",
246
+ "description": "Boolean (true/false, 1/0, yes/no).",
247
+ "examples": [true, false],
248
+ "default": false
249
+ },
250
+
251
+ {
252
+ "name": "string",
253
+ "type": "string",
254
+ "description": "Arbitrary string.",
255
+ "examples": ["Copyright 2026", "my-app", "free text"],
256
+ "constraints": {
257
+ "minLength": "value.length >= minLength",
258
+ "maxLength": "value.length <= maxLength",
259
+ "pattern": "new RegExp(pattern).test(value) // pattern should be a valid JS/PCRE regex string"
260
+ }
261
+ }
262
+ ]
263
+ }