@ontrails/config 1.0.0-beta.12

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 (159) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/.turbo/turbo-lint.log +3 -0
  3. package/.turbo/turbo-typecheck.log +1 -0
  4. package/CHANGELOG.md +19 -0
  5. package/dist/app-config.d.ts +65 -0
  6. package/dist/app-config.d.ts.map +1 -0
  7. package/dist/app-config.js +172 -0
  8. package/dist/app-config.js.map +1 -0
  9. package/dist/collect.d.ts +11 -0
  10. package/dist/collect.d.ts.map +1 -0
  11. package/dist/collect.js +81 -0
  12. package/dist/collect.js.map +1 -0
  13. package/dist/compose.d.ts +26 -0
  14. package/dist/compose.d.ts.map +1 -0
  15. package/dist/compose.js +19 -0
  16. package/dist/compose.js.map +1 -0
  17. package/dist/config-layer.d.ts +11 -0
  18. package/dist/config-layer.d.ts.map +1 -0
  19. package/dist/config-layer.js +6 -0
  20. package/dist/config-layer.js.map +1 -0
  21. package/dist/config-service.d.ts +3 -0
  22. package/dist/config-service.d.ts.map +1 -0
  23. package/dist/config-service.js +26 -0
  24. package/dist/config-service.js.map +1 -0
  25. package/dist/define-config.d.ts +61 -0
  26. package/dist/define-config.d.ts.map +1 -0
  27. package/dist/define-config.js +90 -0
  28. package/dist/define-config.js.map +1 -0
  29. package/dist/describe.d.ts +25 -0
  30. package/dist/describe.d.ts.map +1 -0
  31. package/dist/describe.js +147 -0
  32. package/dist/describe.js.map +1 -0
  33. package/dist/doctor.d.ts +27 -0
  34. package/dist/doctor.d.ts.map +1 -0
  35. package/dist/doctor.js +167 -0
  36. package/dist/doctor.js.map +1 -0
  37. package/dist/explain.d.ts +30 -0
  38. package/dist/explain.d.ts.map +1 -0
  39. package/dist/explain.js +114 -0
  40. package/dist/explain.js.map +1 -0
  41. package/dist/extensions.d.ts +38 -0
  42. package/dist/extensions.d.ts.map +1 -0
  43. package/dist/extensions.js +35 -0
  44. package/dist/extensions.js.map +1 -0
  45. package/dist/generate/env.d.ts +15 -0
  46. package/dist/generate/env.d.ts.map +1 -0
  47. package/dist/generate/env.js +65 -0
  48. package/dist/generate/env.js.map +1 -0
  49. package/dist/generate/example.d.ts +16 -0
  50. package/dist/generate/example.d.ts.map +1 -0
  51. package/dist/generate/example.js +136 -0
  52. package/dist/generate/example.js.map +1 -0
  53. package/dist/generate/helpers.d.ts +35 -0
  54. package/dist/generate/helpers.d.ts.map +1 -0
  55. package/dist/generate/helpers.js +116 -0
  56. package/dist/generate/helpers.js.map +1 -0
  57. package/dist/generate/index.d.ts +4 -0
  58. package/dist/generate/index.d.ts.map +1 -0
  59. package/dist/generate/index.js +4 -0
  60. package/dist/generate/index.js.map +1 -0
  61. package/dist/generate/json-schema.d.ts +18 -0
  62. package/dist/generate/json-schema.d.ts.map +1 -0
  63. package/dist/generate/json-schema.js +97 -0
  64. package/dist/generate/json-schema.js.map +1 -0
  65. package/dist/index.d.ts +21 -0
  66. package/dist/index.d.ts.map +1 -0
  67. package/dist/index.js +21 -0
  68. package/dist/index.js.map +1 -0
  69. package/dist/merge.d.ts +16 -0
  70. package/dist/merge.d.ts.map +1 -0
  71. package/dist/merge.js +34 -0
  72. package/dist/merge.js.map +1 -0
  73. package/dist/ref.d.ts +24 -0
  74. package/dist/ref.d.ts.map +1 -0
  75. package/dist/ref.js +25 -0
  76. package/dist/ref.js.map +1 -0
  77. package/dist/registry.d.ts +24 -0
  78. package/dist/registry.d.ts.map +1 -0
  79. package/dist/registry.js +12 -0
  80. package/dist/registry.js.map +1 -0
  81. package/dist/resolve.d.ts +21 -0
  82. package/dist/resolve.d.ts.map +1 -0
  83. package/dist/resolve.js +174 -0
  84. package/dist/resolve.js.map +1 -0
  85. package/dist/secret-heuristics.d.ts +10 -0
  86. package/dist/secret-heuristics.d.ts.map +1 -0
  87. package/dist/secret-heuristics.js +11 -0
  88. package/dist/secret-heuristics.js.map +1 -0
  89. package/dist/trails/config-check.d.ts +11 -0
  90. package/dist/trails/config-check.d.ts.map +1 -0
  91. package/dist/trails/config-check.js +53 -0
  92. package/dist/trails/config-check.js.map +1 -0
  93. package/dist/trails/config-describe.d.ts +12 -0
  94. package/dist/trails/config-describe.d.ts.map +1 -0
  95. package/dist/trails/config-describe.js +41 -0
  96. package/dist/trails/config-describe.js.map +1 -0
  97. package/dist/trails/config-explain.d.ts +8 -0
  98. package/dist/trails/config-explain.d.ts.map +1 -0
  99. package/dist/trails/config-explain.js +74 -0
  100. package/dist/trails/config-explain.js.map +1 -0
  101. package/dist/trails/config-init.d.ts +9 -0
  102. package/dist/trails/config-init.d.ts.map +1 -0
  103. package/dist/trails/config-init.js +78 -0
  104. package/dist/trails/config-init.js.map +1 -0
  105. package/dist/workspace.d.ts +9 -0
  106. package/dist/workspace.d.ts.map +1 -0
  107. package/dist/workspace.js +44 -0
  108. package/dist/workspace.js.map +1 -0
  109. package/dist/zod-utils.d.ts +14 -0
  110. package/dist/zod-utils.d.ts.map +1 -0
  111. package/dist/zod-utils.js +41 -0
  112. package/dist/zod-utils.js.map +1 -0
  113. package/package.json +20 -0
  114. package/src/__tests__/app-config.test.ts +329 -0
  115. package/src/__tests__/compose.test.ts +59 -0
  116. package/src/__tests__/config-check.test.ts +171 -0
  117. package/src/__tests__/config-describe.test.ts +154 -0
  118. package/src/__tests__/config-explain.test.ts +167 -0
  119. package/src/__tests__/config-init.test.ts +210 -0
  120. package/src/__tests__/config-layer.test.ts +53 -0
  121. package/src/__tests__/config-service.test.ts +87 -0
  122. package/src/__tests__/define-config.test.ts +263 -0
  123. package/src/__tests__/describe.test.ts +158 -0
  124. package/src/__tests__/doctor.test.ts +172 -0
  125. package/src/__tests__/explain.test.ts +139 -0
  126. package/src/__tests__/extensions.test.ts +134 -0
  127. package/src/__tests__/generate.test.ts +269 -0
  128. package/src/__tests__/ref.test.ts +35 -0
  129. package/src/__tests__/resolve.test.ts +246 -0
  130. package/src/__tests__/workspace.test.ts +64 -0
  131. package/src/app-config.ts +307 -0
  132. package/src/collect.ts +118 -0
  133. package/src/compose.ts +46 -0
  134. package/src/config-layer.ts +15 -0
  135. package/src/config-service.ts +32 -0
  136. package/src/define-config.ts +134 -0
  137. package/src/describe.ts +252 -0
  138. package/src/doctor.ts +219 -0
  139. package/src/explain.ts +176 -0
  140. package/src/extensions.ts +51 -0
  141. package/src/generate/env.ts +104 -0
  142. package/src/generate/example.ts +222 -0
  143. package/src/generate/helpers.ts +158 -0
  144. package/src/generate/index.ts +3 -0
  145. package/src/generate/json-schema.ts +137 -0
  146. package/src/index.ts +44 -0
  147. package/src/merge.ts +43 -0
  148. package/src/ref.ts +38 -0
  149. package/src/registry.ts +33 -0
  150. package/src/resolve.ts +279 -0
  151. package/src/secret-heuristics.ts +13 -0
  152. package/src/trails/config-check.ts +60 -0
  153. package/src/trails/config-describe.ts +44 -0
  154. package/src/trails/config-explain.ts +93 -0
  155. package/src/trails/config-init.ts +96 -0
  156. package/src/workspace.ts +51 -0
  157. package/src/zod-utils.ts +53 -0
  158. package/tsconfig.json +9 -0
  159. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Infrastructure trail that exposes config provenance.
3
+ *
4
+ * Returns resolved config entries with source information so agents
5
+ * and operators can answer "where did this value come from?"
6
+ */
7
+ import { Result, trail } from '@ontrails/core';
8
+ import { z } from 'zod';
9
+ import { configService } from '../config-service.js';
10
+ import { explainConfig } from '../explain.js';
11
+ const provenanceEntrySchema = z.object({
12
+ path: z.string(),
13
+ redacted: z.boolean(),
14
+ source: z.string(),
15
+ value: z.unknown(),
16
+ });
17
+ const outputSchema = z.object({
18
+ entries: z.array(provenanceEntrySchema),
19
+ });
20
+ /** Filter provenance entries by path prefix when specified. */
21
+ const filterByPath = (entries, prefix) => prefix
22
+ ? entries.filter((entry) => entry.path === prefix || entry.path.startsWith(`${prefix}.`))
23
+ : entries;
24
+ /** Build ExplainConfigOptions from ConfigState, omitting undefined layers. */
25
+ const toExplainOptions = (state) => {
26
+ const base = {
27
+ resolved: state.resolved,
28
+ schema: state.schema,
29
+ };
30
+ if (state.base) {
31
+ return { ...base, base: state.base };
32
+ }
33
+ return base;
34
+ };
35
+ /** Enrich explain options with env and layer overrides from state. */
36
+ const enrichOptions = (state, options) => {
37
+ let enriched = options;
38
+ if (state.env) {
39
+ enriched = { ...enriched, env: state.env };
40
+ }
41
+ if (state.loadout) {
42
+ enriched = { ...enriched, loadout: state.loadout };
43
+ }
44
+ if (state.local) {
45
+ enriched = { ...enriched, local: state.local };
46
+ }
47
+ return enriched;
48
+ };
49
+ export const configExplain = trail('config.explain', {
50
+ examples: [
51
+ {
52
+ input: {},
53
+ name: 'Explain all fields',
54
+ },
55
+ ],
56
+ input: z.object({
57
+ path: z
58
+ .string()
59
+ .describe('Config field path to explain (or empty for all)')
60
+ .default(''),
61
+ }),
62
+ intent: 'read',
63
+ metadata: { category: 'infrastructure' },
64
+ output: outputSchema,
65
+ run: (input, ctx) => {
66
+ const state = configService.from(ctx);
67
+ const options = enrichOptions(state, toExplainOptions(state));
68
+ const entries = explainConfig(options);
69
+ const filtered = filterByPath(entries, input.path);
70
+ return Result.ok({ entries: [...filtered] });
71
+ },
72
+ services: [configService],
73
+ });
74
+ //# sourceMappingURL=config-explain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-explain.js","sourceRoot":"","sources":["../../src/trails/config-explain.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAG9C,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IACrC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE;IACrB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;IAClB,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE;CACnB,CAAC,CAAC;AAEH,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5B,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,qBAAqB,CAAC;CACxC,CAAC,CAAC;AAEH,+DAA+D;AAC/D,MAAM,YAAY,GAAG,CACnB,OAA6C,EAC7C,MAAc,EACwB,EAAE,CACxC,MAAM;IACJ,CAAC,CAAC,OAAO,CAAC,MAAM,CACZ,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,MAAM,GAAG,CAAC,CACxE;IACH,CAAC,CAAC,OAAO,CAAC;AAEd,8EAA8E;AAC9E,MAAM,gBAAgB,GAAG,CACvB,KAAkB,EACyB,EAAE;IAC7C,MAAM,IAAI,GAA8C;QACtD,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,MAAM,EAAE,KAAK,CAAC,MAAM;KACrB,CAAC;IACF,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QACf,OAAO,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;IACvC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF,sEAAsE;AACtE,MAAM,aAAa,GAAG,CACpB,KAAkB,EAClB,OAAkD,EACP,EAAE;IAC7C,IAAI,QAAQ,GAAG,OAAO,CAAC;IACvB,IAAI,KAAK,CAAC,GAAG,EAAE,CAAC;QACd,QAAQ,GAAG,EAAE,GAAG,QAAQ,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC;IAC7C,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QAClB,QAAQ,GAAG,EAAE,GAAG,QAAQ,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;IACrD,CAAC;IACD,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;QAChB,QAAQ,GAAG,EAAE,GAAG,QAAQ,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;IACjD,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,CAAC,gBAAgB,EAAE;IACnD,QAAQ,EAAE;QACR;YACE,KAAK,EAAE,EAAE;YACT,IAAI,EAAE,oBAAoB;SAC3B;KACF;IACD,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC;QACd,IAAI,EAAE,CAAC;aACJ,MAAM,EAAE;aACR,QAAQ,CAAC,iDAAiD,CAAC;aAC3D,OAAO,CAAC,EAAE,CAAC;KACf,CAAC;IACF,MAAM,EAAE,MAAM;IACd,QAAQ,EAAE,EAAE,QAAQ,EAAE,gBAAgB,EAAE;IACxC,MAAM,EAAE,YAAY;IACpB,GAAG,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QAClB,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACtC,MAAM,OAAO,GAAG,aAAa,CAAC,KAAK,EAAE,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC;QAC9D,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;QACvC,MAAM,QAAQ,GAAG,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QACnD,OAAO,MAAM,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;IAC/C,CAAC;IACD,QAAQ,EAAE,CAAC,aAAa,CAAC;CAC1B,CAAC,CAAC"}
@@ -0,0 +1,9 @@
1
+ export declare const configInit: import("@ontrails/core").Trail<{
2
+ format: "toml" | "json" | "jsonc" | "yaml";
3
+ dir?: string | undefined;
4
+ }, {
5
+ content: string;
6
+ format: string;
7
+ writtenFiles?: string[] | undefined;
8
+ }>;
9
+ //# sourceMappingURL=config-init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-init.d.ts","sourceRoot":"","sources":["../../src/trails/config-init.ts"],"names":[],"mappings":"AA+DA,eAAO,MAAM,UAAU;;;;;;;EAgCrB,CAAC"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Infrastructure trail that generates an example config file.
3
+ *
4
+ * Produces TOML, JSON, JSONC, or YAML output from the registered
5
+ * config schema, with defaults shown and deprecated fields annotated.
6
+ *
7
+ * When `dir` is provided, also writes `.env.example` and `.schema.json`
8
+ * to the specified directory.
9
+ */
10
+ import { join } from 'node:path';
11
+ import { mkdir } from 'node:fs/promises';
12
+ import { Result, trail } from '@ontrails/core';
13
+ import { z as zod } from 'zod';
14
+ import { configService } from '../config-service.js';
15
+ import { generateEnvExample, generateExample, generateJsonSchema, } from '../generate/index.js';
16
+ const formatEnum = zod.enum(['toml', 'json', 'jsonc', 'yaml']);
17
+ const outputSchema = zod.object({
18
+ content: zod.string(),
19
+ format: zod.string(),
20
+ writtenFiles: zod.array(zod.string()).optional(),
21
+ });
22
+ /** Collect artifacts to write: [relativeName, content] pairs. */
23
+ const collectArtifacts = (schema) => {
24
+ const artifacts = [];
25
+ const envContent = generateEnvExample(schema);
26
+ if (envContent.length > 0) {
27
+ artifacts.push(['.env.example', envContent]);
28
+ }
29
+ artifacts.push([
30
+ '.schema.json',
31
+ JSON.stringify(generateJsonSchema(schema), null, 2),
32
+ ]);
33
+ return artifacts;
34
+ };
35
+ /** Write generated artifacts to the target directory. */
36
+ const writeArtifacts = async (dir, schema) => {
37
+ await mkdir(dir, { recursive: true });
38
+ const artifacts = collectArtifacts(schema);
39
+ const written = [];
40
+ for (const [name, content] of artifacts) {
41
+ const fullPath = join(dir, name);
42
+ await Bun.write(fullPath, content);
43
+ written.push(fullPath);
44
+ }
45
+ return written;
46
+ };
47
+ export const configInit = trail('config.init', {
48
+ examples: [
49
+ {
50
+ input: {},
51
+ name: 'Generate TOML example',
52
+ },
53
+ ],
54
+ input: zod.object({
55
+ dir: zod
56
+ .string()
57
+ .describe('Directory to write generated artifacts to')
58
+ .optional(),
59
+ format: formatEnum
60
+ .describe('Output format for the example config file')
61
+ .default('toml'),
62
+ }),
63
+ intent: 'write',
64
+ metadata: { category: 'infrastructure' },
65
+ output: outputSchema,
66
+ run: async (input, ctx) => {
67
+ const state = configService.from(ctx);
68
+ const schema = state.schema;
69
+ const content = generateExample(schema, input.format);
70
+ if (input.dir) {
71
+ const writtenFiles = await writeArtifacts(input.dir, schema);
72
+ return Result.ok({ content, format: input.format, writtenFiles });
73
+ }
74
+ return Result.ok({ content, format: input.format });
75
+ },
76
+ services: [configService],
77
+ });
78
+ //# sourceMappingURL=config-init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-init.js","sourceRoot":"","sources":["../../src/trails/config-init.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Ensure the `.trails/` workspace directory exists with proper structure.
3
+ *
4
+ * Creates `config/`, `dev/`, and `generated/` subdirectories plus a
5
+ * `.gitignore` that excludes local-only files. Safe to call repeatedly —
6
+ * existing files are never overwritten.
7
+ */
8
+ export declare const ensureWorkspace: (root: string) => Promise<void>;
9
+ //# sourceMappingURL=workspace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAmCA;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAAU,MAAM,MAAM,KAAG,OAAO,CAAC,IAAI,CAQhE,CAAC"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Ensure the `.trails/` workspace directory exists with proper structure.
3
+ *
4
+ * Auto-creates on first framework operation. The workspace holds local
5
+ * config overrides, development state, and generated artifacts that
6
+ * should not be committed to source control.
7
+ */
8
+ import { mkdir } from 'node:fs/promises';
9
+ import { join } from 'node:path';
10
+ const WORKSPACE_DIRS = ['config', 'dev', 'generated'];
11
+ const GITIGNORE_CONTENT = [
12
+ '# Local config overrides',
13
+ 'config/',
14
+ '',
15
+ '# Development state',
16
+ 'dev/',
17
+ '',
18
+ '# Generated artifacts',
19
+ 'generated/',
20
+ '',
21
+ ].join('\n');
22
+ /**
23
+ * Write `.gitignore` inside the workspace directory if it does not already exist.
24
+ */
25
+ const writeGitignoreIfMissing = async (trailsDir) => {
26
+ const gitignorePath = join(trailsDir, '.gitignore');
27
+ const file = Bun.file(gitignorePath);
28
+ if (!(await file.exists())) {
29
+ await Bun.write(gitignorePath, GITIGNORE_CONTENT);
30
+ }
31
+ };
32
+ /**
33
+ * Ensure the `.trails/` workspace directory exists with proper structure.
34
+ *
35
+ * Creates `config/`, `dev/`, and `generated/` subdirectories plus a
36
+ * `.gitignore` that excludes local-only files. Safe to call repeatedly —
37
+ * existing files are never overwritten.
38
+ */
39
+ export const ensureWorkspace = async (root) => {
40
+ const trailsDir = join(root, '.trails');
41
+ await Promise.all(WORKSPACE_DIRS.map((d) => mkdir(join(trailsDir, d), { recursive: true })));
42
+ await writeGitignoreIfMissing(trailsDir);
43
+ };
44
+ //# sourceMappingURL=workspace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.js","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,cAAc,GAAG,CAAC,QAAQ,EAAE,KAAK,EAAE,WAAW,CAAU,CAAC;AAE/D,MAAM,iBAAiB,GAAG;IACxB,0BAA0B;IAC1B,SAAS;IACT,EAAE;IACF,qBAAqB;IACrB,MAAM;IACN,EAAE;IACF,uBAAuB;IACvB,YAAY;IACZ,EAAE;CACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb;;GAEG;AACH,MAAM,uBAAuB,GAAG,KAAK,EAAE,SAAiB,EAAiB,EAAE;IACzE,MAAM,aAAa,GAAG,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IACpD,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IACrC,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC;QAC3B,MAAM,GAAG,CAAC,KAAK,CAAC,aAAa,EAAE,iBAAiB,CAAC,CAAC;IACpD,CAAC;AACH,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,EAAE,IAAY,EAAiB,EAAE;IACnE,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAExC,MAAM,OAAO,CAAC,GAAG,CACf,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,CAC1E,CAAC;IAEF,MAAM,uBAAuB,CAAC,SAAS,CAAC,CAAC;AAC3C,CAAC,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared Zod introspection helpers used by config resolution, doctor,
3
+ * describe, explain, and collect modules.
4
+ */
5
+ import type { z } from 'zod';
6
+ /** Extract the Zod def record from any ZodType. */
7
+ export declare const zodDef: (schema: z.ZodType) => Record<string, unknown>;
8
+ /** Unwrap through optional/default/nullable wrappers to find the base schema. */
9
+ export declare const unwrapToBase: (schema: z.ZodType) => z.ZodType;
10
+ /** Check if a schema is (or wraps) a ZodObject by inspecting its def. */
11
+ export declare const isZodObject: (schema: z.ZodType) => schema is z.ZodObject<Record<string, z.ZodType>>;
12
+ /** Read a value at a dot-separated path from a plain object. */
13
+ export declare const getAtPath: (obj: Record<string, unknown>, path: string) => unknown;
14
+ //# sourceMappingURL=zod-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zod-utils.d.ts","sourceRoot":"","sources":["../src/zod-utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,mDAAmD;AACnD,eAAO,MAAM,MAAM,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CACf,CAAC;AAKnD,iFAAiF;AACjF,eAAO,MAAM,YAAY,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,CAAC,CAAC,OAclD,CAAC;AAEF,yEAAyE;AACzE,eAAO,MAAM,WAAW,GACtB,QAAQ,CAAC,CAAC,OAAO,KAChB,MAAM,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAGjD,CAAC;AAEF,gEAAgE;AAChE,eAAO,MAAM,SAAS,GACpB,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,MAAM,MAAM,KACX,OASF,CAAC"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Shared Zod introspection helpers used by config resolution, doctor,
3
+ * describe, explain, and collect modules.
4
+ */
5
+ /** Extract the Zod def record from any ZodType. */
6
+ export const zodDef = (schema) => schema.def;
7
+ /** Wrapper types that should be peeled before checking the base type. */
8
+ const WRAPPER_TYPES = new Set(['optional', 'default', 'nullable']);
9
+ /** Unwrap through optional/default/nullable wrappers to find the base schema. */
10
+ export const unwrapToBase = (schema) => {
11
+ let current = schema;
12
+ for (let depth = 0; depth < 10; depth += 1) {
13
+ const def = zodDef(current);
14
+ if (!WRAPPER_TYPES.has(def['type'])) {
15
+ return current;
16
+ }
17
+ const inner = def['innerType'];
18
+ if (!inner) {
19
+ return current;
20
+ }
21
+ current = inner;
22
+ }
23
+ return current;
24
+ };
25
+ /** Check if a schema is (or wraps) a ZodObject by inspecting its def. */
26
+ export const isZodObject = (schema) => {
27
+ const def = zodDef(unwrapToBase(schema));
28
+ return def['type'] === 'object' && 'shape' in def;
29
+ };
30
+ /** Read a value at a dot-separated path from a plain object. */
31
+ export const getAtPath = (obj, path) => {
32
+ let current = obj;
33
+ for (const part of path.split('.')) {
34
+ if (typeof current !== 'object' || current === null) {
35
+ return undefined;
36
+ }
37
+ current = current[part];
38
+ }
39
+ return current;
40
+ };
41
+ //# sourceMappingURL=zod-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zod-utils.js","sourceRoot":"","sources":["../src/zod-utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,mDAAmD;AACnD,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,MAAiB,EAA2B,EAAE,CACnE,MAAM,CAAC,GAAyC,CAAC;AAEnD,yEAAyE;AACzE,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC;AAEnE,iFAAiF;AACjF,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAAiB,EAAa,EAAE;IAC3D,IAAI,OAAO,GAAG,MAAM,CAAC;IACrB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,EAAE,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QAC3C,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;QAC5B,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAW,CAAC,EAAE,CAAC;YAC9C,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,MAAM,KAAK,GAAG,GAAG,CAAC,WAAW,CAA0B,CAAC;QACxD,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,OAAO,GAAG,KAAK,CAAC;IAClB,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC,CAAC;AAEF,yEAAyE;AACzE,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,MAAiB,EACiC,EAAE;IACpD,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC;IACzC,OAAO,GAAG,CAAC,MAAM,CAAC,KAAK,QAAQ,IAAI,OAAO,IAAI,GAAG,CAAC;AACpD,CAAC,CAAC;AAEF,gEAAgE;AAChE,MAAM,CAAC,MAAM,SAAS,GAAG,CACvB,GAA4B,EAC5B,IAAY,EACH,EAAE;IACX,IAAI,OAAO,GAAY,GAAG,CAAC;IAC3B,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACnC,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACpD,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,GAAI,OAAmC,CAAC,IAAI,CAAC,CAAC;IACvD,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@ontrails/config",
3
+ "version": "1.0.0-beta.12",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.ts",
7
+ "./package.json": "./package.json"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc -b",
11
+ "test": "bun test",
12
+ "typecheck": "tsc --noEmit",
13
+ "lint": "oxlint ./src",
14
+ "clean": "rm -rf dist *.tsbuildinfo"
15
+ },
16
+ "peerDependencies": {
17
+ "@ontrails/core": "^1.0.0-beta.12",
18
+ "zod": "^4.3.5"
19
+ }
20
+ }
@@ -0,0 +1,329 @@
1
+ import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
2
+ import { mkdir, mkdtemp, rm } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { z } from 'zod';
6
+
7
+ import type { AppConfig } from '../app-config.js';
8
+ import { appConfig } from '../app-config.js';
9
+ import { describeConfig } from '../describe.js';
10
+ import { checkConfig } from '../doctor.js';
11
+ import { configRef } from '../ref.js';
12
+
13
+ const testSchema = z.object({
14
+ output: z.string().describe('Output directory').default('./output'),
15
+ verbose: z.boolean().default(false),
16
+ });
17
+
18
+ const createJsonConfig = () =>
19
+ appConfig('myapp', {
20
+ formats: ['json'],
21
+ schema: testSchema,
22
+ });
23
+
24
+ const writeJsonConfig = (filePath: string, output: string) =>
25
+ Bun.write(filePath, JSON.stringify({ output }));
26
+
27
+ const resolveOutput = async (
28
+ config: AppConfig<typeof testSchema>,
29
+ filePath: string
30
+ ): Promise<string> => {
31
+ const result = await config.resolve({ path: filePath });
32
+ expect(result.isOk()).toBe(true);
33
+ return result.unwrap().output;
34
+ };
35
+
36
+ describe('appConfig()', () => {
37
+ describe('creation', () => {
38
+ test('returns an object with the provided name', () => {
39
+ const config = appConfig('myapp', { schema: testSchema });
40
+ expect(config.name).toBe('myapp');
41
+ });
42
+
43
+ test('returns the provided schema', () => {
44
+ const config = appConfig('myapp', { schema: testSchema });
45
+ expect(config.schema).toBe(testSchema);
46
+ });
47
+
48
+ test('defaults formats to toml, json, yaml when not specified', () => {
49
+ const config = appConfig('myapp', { schema: testSchema });
50
+ expect(config.formats).toEqual(['toml', 'json', 'yaml']);
51
+ });
52
+
53
+ test('uses provided formats', () => {
54
+ const config = appConfig('myapp', {
55
+ formats: ['json', 'jsonc'],
56
+ schema: testSchema,
57
+ });
58
+ expect(config.formats).toEqual(['json', 'jsonc']);
59
+ });
60
+
61
+ test('defaults dotfile to false when not specified', () => {
62
+ const config = appConfig('myapp', { schema: testSchema });
63
+ expect(config.dotfile).toBe(false);
64
+ });
65
+
66
+ test('uses provided dotfile value', () => {
67
+ const config = appConfig('myapp', {
68
+ dotfile: true,
69
+ schema: testSchema,
70
+ });
71
+ expect(config.dotfile).toBe(true);
72
+ });
73
+ });
74
+
75
+ describe('resolve() with explicit path', () => {
76
+ let tempDir: string;
77
+
78
+ beforeEach(async () => {
79
+ tempDir = await mkdtemp(join(tmpdir(), 'trails-config-'));
80
+ });
81
+
82
+ afterEach(async () => {
83
+ await rm(tempDir, { force: true, recursive: true });
84
+ });
85
+
86
+ test('reads and validates a JSON config file', async () => {
87
+ const filePath = join(tempDir, 'myapp.config.json');
88
+ await Bun.write(
89
+ filePath,
90
+ JSON.stringify({ output: './dist', verbose: true })
91
+ );
92
+
93
+ const config = createJsonConfig();
94
+ const result = await config.resolve({ path: filePath });
95
+
96
+ expect(result.isOk()).toBe(true);
97
+ expect(result.unwrap()).toEqual({ output: './dist', verbose: true });
98
+ });
99
+
100
+ test('reads and validates a TOML config file', async () => {
101
+ const filePath = join(tempDir, 'myapp.config.toml');
102
+ await Bun.write(filePath, 'output = "./build"\nverbose = true\n');
103
+
104
+ const config = appConfig('myapp', {
105
+ formats: ['toml'],
106
+ schema: testSchema,
107
+ });
108
+ const result = await config.resolve({ path: filePath });
109
+
110
+ expect(result.isOk()).toBe(true);
111
+ expect(result.unwrap()).toEqual({ output: './build', verbose: true });
112
+ });
113
+
114
+ test('applies schema defaults for missing fields', async () => {
115
+ const filePath = join(tempDir, 'myapp.config.json');
116
+ await Bun.write(filePath, JSON.stringify({}));
117
+
118
+ const config = createJsonConfig();
119
+ const result = await config.resolve({ path: filePath });
120
+
121
+ expect(result.isOk()).toBe(true);
122
+ expect(result.unwrap()).toEqual({ output: './output', verbose: false });
123
+ });
124
+
125
+ test('re-reads config files after they change on disk', async () => {
126
+ const filePath = join(tempDir, 'myapp.config.json');
127
+ await writeJsonConfig(filePath, './first');
128
+
129
+ const config = createJsonConfig();
130
+
131
+ expect(await resolveOutput(config, filePath)).toBe('./first');
132
+
133
+ await Bun.sleep(10);
134
+ await writeJsonConfig(filePath, './second');
135
+
136
+ expect(await resolveOutput(config, filePath)).toBe('./second');
137
+ });
138
+
139
+ test('returns Result.err when file does not exist', async () => {
140
+ const filePath = join(tempDir, 'nonexistent.json');
141
+
142
+ const config = createJsonConfig();
143
+ const result = await config.resolve({ path: filePath });
144
+
145
+ expect(result.isErr()).toBe(true);
146
+ });
147
+
148
+ test('returns Result.err when file has invalid content', async () => {
149
+ const filePath = join(tempDir, 'myapp.config.json');
150
+ await Bun.write(
151
+ filePath,
152
+ JSON.stringify({ output: 42, verbose: 'not-a-bool' })
153
+ );
154
+
155
+ const config = createJsonConfig();
156
+ const result = await config.resolve({ path: filePath });
157
+
158
+ expect(result.isErr()).toBe(true);
159
+ });
160
+ });
161
+
162
+ describe('resolve() with discovery', () => {
163
+ let tempDir: string;
164
+
165
+ beforeEach(async () => {
166
+ tempDir = await mkdtemp(join(tmpdir(), 'trails-config-'));
167
+ });
168
+
169
+ afterEach(async () => {
170
+ await rm(tempDir, { force: true, recursive: true });
171
+ });
172
+
173
+ test('discovers config file in cwd', async () => {
174
+ const filePath = join(tempDir, 'myapp.config.json');
175
+ await writeJsonConfig(filePath, './found');
176
+
177
+ const config = createJsonConfig();
178
+ const result = await config.resolve({ cwd: tempDir });
179
+
180
+ expect(result.isOk()).toBe(true);
181
+ expect(result.unwrap().output).toBe('./found');
182
+ });
183
+
184
+ test('discovers config file by walking up directories', async () => {
185
+ const nested = join(tempDir, 'a', 'b', 'c');
186
+ await mkdir(nested, { recursive: true });
187
+
188
+ const filePath = join(tempDir, 'myapp.config.json');
189
+ await Bun.write(filePath, JSON.stringify({ output: './parent' }));
190
+
191
+ const config = appConfig('myapp', {
192
+ formats: ['json'],
193
+ schema: testSchema,
194
+ });
195
+ const result = await config.resolve({ cwd: nested });
196
+
197
+ expect(result.isOk()).toBe(true);
198
+ expect(result.unwrap().output).toBe('./parent');
199
+ });
200
+
201
+ test('tries formats in order', async () => {
202
+ await Bun.write(
203
+ join(tempDir, 'myapp.config.toml'),
204
+ 'output = "./from-toml"\n'
205
+ );
206
+ await Bun.write(
207
+ join(tempDir, 'myapp.config.json'),
208
+ JSON.stringify({ output: './from-json' })
209
+ );
210
+
211
+ const config = appConfig('myapp', {
212
+ formats: ['toml', 'json'],
213
+ schema: testSchema,
214
+ });
215
+ const result = await config.resolve({ cwd: tempDir });
216
+
217
+ expect(result.isOk()).toBe(true);
218
+ expect(result.unwrap().output).toBe('./from-toml');
219
+ });
220
+
221
+ test('returns Result.err when no config file found', async () => {
222
+ const config = appConfig('myapp', { schema: testSchema });
223
+ const result = await config.resolve({ cwd: tempDir });
224
+
225
+ expect(result.isErr()).toBe(true);
226
+ });
227
+ });
228
+
229
+ describe('file naming conventions', () => {
230
+ let tempDir: string;
231
+
232
+ beforeEach(async () => {
233
+ tempDir = await mkdtemp(join(tmpdir(), 'trails-config-'));
234
+ });
235
+
236
+ afterEach(async () => {
237
+ await rm(tempDir, { force: true, recursive: true });
238
+ });
239
+
240
+ test('dotfile: true searches for .myapprc.* files', async () => {
241
+ await Bun.write(
242
+ join(tempDir, '.myapprc.json'),
243
+ JSON.stringify({ output: './dotfile' })
244
+ );
245
+
246
+ const config = appConfig('myapp', {
247
+ dotfile: true,
248
+ formats: ['json'],
249
+ schema: testSchema,
250
+ });
251
+ const result = await config.resolve({ cwd: tempDir });
252
+
253
+ expect(result.isOk()).toBe(true);
254
+ expect(result.unwrap().output).toBe('./dotfile');
255
+ });
256
+
257
+ test('dotfile: false searches for myapp.config.* files', async () => {
258
+ await Bun.write(
259
+ join(tempDir, 'myapp.config.json'),
260
+ JSON.stringify({ output: './standard' })
261
+ );
262
+
263
+ const config = appConfig('myapp', {
264
+ dotfile: false,
265
+ formats: ['json'],
266
+ schema: testSchema,
267
+ });
268
+ const result = await config.resolve({ cwd: tempDir });
269
+
270
+ expect(result.isOk()).toBe(true);
271
+ expect(result.unwrap().output).toBe('./standard');
272
+ });
273
+
274
+ test('dotfile: true does NOT find myapp.config.* files', async () => {
275
+ await Bun.write(
276
+ join(tempDir, 'myapp.config.json'),
277
+ JSON.stringify({ output: './nope' })
278
+ );
279
+
280
+ const config = appConfig('myapp', {
281
+ dotfile: true,
282
+ formats: ['json'],
283
+ schema: testSchema,
284
+ });
285
+ const result = await config.resolve({ cwd: tempDir });
286
+
287
+ expect(result.isErr()).toBe(true);
288
+ });
289
+ });
290
+
291
+ describe('method delegations', () => {
292
+ test('describe() returns same result as describeConfig(schema)', () => {
293
+ const config = appConfig('myapp', { schema: testSchema });
294
+ const methodResult = config.describe();
295
+ const standaloneResult = describeConfig(testSchema);
296
+
297
+ expect(methodResult).toEqual(standaloneResult);
298
+ });
299
+
300
+ test('check() returns same result as checkConfig(schema, values)', () => {
301
+ const config = appConfig('myapp', { schema: testSchema });
302
+ const values = { output: './dist', verbose: true };
303
+
304
+ const methodResult = config.check(values);
305
+ const standaloneResult = checkConfig(testSchema, values);
306
+
307
+ expect(methodResult).toEqual(standaloneResult);
308
+ });
309
+
310
+ test('ref() returns same result as configRef(path)', () => {
311
+ const config = appConfig('myapp', { schema: testSchema });
312
+ const methodResult = config.ref('output');
313
+ const standaloneResult = configRef('output');
314
+
315
+ expect(methodResult).toEqual(standaloneResult);
316
+ });
317
+
318
+ test('explain() delegates to explainConfig with bound schema', () => {
319
+ const config = appConfig('myapp', { schema: testSchema });
320
+ const resolved = { output: './dist', verbose: true };
321
+
322
+ const result = config.explain({ resolved });
323
+
324
+ expect(result.length).toBeGreaterThan(0);
325
+ expect(result[0]?.path).toBeDefined();
326
+ expect(result[0]?.source).toBeDefined();
327
+ });
328
+ });
329
+ });