@config-bound/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-format$colon$ci.log +6 -0
- package/.turbo/turbo-lint$colon$ci.log +4 -0
- package/.turbo/turbo-test.log +19 -0
- package/CHANGELOG.md +15 -0
- package/README.md +66 -0
- package/bin/configbound.js +3 -0
- package/dist/cli.module.d.ts +3 -0
- package/dist/cli.module.d.ts.map +1 -0
- package/dist/cli.module.js +29 -0
- package/dist/cli.module.js.map +1 -0
- package/dist/commands/export.command.d.ts +30 -0
- package/dist/commands/export.command.d.ts.map +1 -0
- package/dist/commands/export.command.js +226 -0
- package/dist/commands/export.command.js.map +1 -0
- package/dist/commands/list.command.d.ts +13 -0
- package/dist/commands/list.command.d.ts.map +1 -0
- package/dist/commands/list.command.js +93 -0
- package/dist/commands/list.command.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +17 -0
- package/dist/main.js.map +1 -0
- package/dist/services/config-discovery.service.d.ts +15 -0
- package/dist/services/config-discovery.service.d.ts.map +1 -0
- package/dist/services/config-discovery.service.js +191 -0
- package/dist/services/config-discovery.service.js.map +1 -0
- package/dist/services/config-discovery.service.spec.d.ts +2 -0
- package/dist/services/config-discovery.service.spec.d.ts.map +1 -0
- package/dist/services/config-discovery.service.spec.js +137 -0
- package/dist/services/config-discovery.service.spec.js.map +1 -0
- package/dist/services/config-loader.service.d.ts +13 -0
- package/dist/services/config-loader.service.d.ts.map +1 -0
- package/dist/services/config-loader.service.js +241 -0
- package/dist/services/config-loader.service.js.map +1 -0
- package/dist/services/file-writer.service.d.ts +6 -0
- package/dist/services/file-writer.service.d.ts.map +1 -0
- package/dist/services/file-writer.service.js +38 -0
- package/dist/services/file-writer.service.js.map +1 -0
- package/dist/services/file-writer.service.spec.d.ts +2 -0
- package/dist/services/file-writer.service.spec.d.ts.map +1 -0
- package/dist/services/file-writer.service.spec.js +98 -0
- package/dist/services/file-writer.service.spec.js.map +1 -0
- package/dist/services/schema-export.service.d.ts +14 -0
- package/dist/services/schema-export.service.d.ts.map +1 -0
- package/dist/services/schema-export.service.js +58 -0
- package/dist/services/schema-export.service.js.map +1 -0
- package/dist/services/schema-export.service.spec.d.ts +2 -0
- package/dist/services/schema-export.service.spec.d.ts.map +1 -0
- package/dist/services/schema-export.service.spec.js +69 -0
- package/dist/services/schema-export.service.spec.js.map +1 -0
- package/eslint.config.mjs +5 -0
- package/jest.config.js +39 -0
- package/package.json +62 -0
- package/src/cli.module.ts +21 -0
- package/src/commands/export.command.ts +253 -0
- package/src/commands/list.command.ts +108 -0
- package/src/main.ts +19 -0
- package/src/services/config-discovery.service.spec.ts +209 -0
- package/src/services/config-discovery.service.ts +237 -0
- package/src/services/config-loader.service.ts +379 -0
- package/src/services/file-writer.service.spec.ts +139 -0
- package/src/services/file-writer.service.ts +32 -0
- package/src/services/schema-export.service.spec.ts +124 -0
- package/src/services/schema-export.service.ts +85 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
2
|
+
import { FileWriterService } from './file-writer.service.js';
|
|
3
|
+
import {
|
|
4
|
+
describe,
|
|
5
|
+
beforeEach,
|
|
6
|
+
afterEach,
|
|
7
|
+
it,
|
|
8
|
+
expect,
|
|
9
|
+
jest
|
|
10
|
+
} from '@jest/globals';
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
|
|
15
|
+
describe('FileWriterService', () => {
|
|
16
|
+
let service: FileWriterService;
|
|
17
|
+
let tempDir: string;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
21
|
+
providers: [FileWriterService]
|
|
22
|
+
}).compile();
|
|
23
|
+
|
|
24
|
+
service = module.get<FileWriterService>(FileWriterService);
|
|
25
|
+
|
|
26
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'configbound-test-'));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
if (fs.existsSync(tempDir)) {
|
|
31
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should be defined', () => {
|
|
36
|
+
expect(service).toBeDefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('writeToFile', () => {
|
|
40
|
+
it('should write content to file', () => {
|
|
41
|
+
const filePath = path.join(tempDir, 'test.txt');
|
|
42
|
+
const content = 'Hello, World!';
|
|
43
|
+
|
|
44
|
+
service.writeToFile(filePath, content);
|
|
45
|
+
|
|
46
|
+
expect(fs.existsSync(filePath)).toBe(true);
|
|
47
|
+
const writtenContent = fs.readFileSync(filePath, 'utf8');
|
|
48
|
+
expect(writtenContent).toBe(content);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should create directories if they do not exist', () => {
|
|
52
|
+
const filePath = path.join(tempDir, 'nested', 'dir', 'test.txt');
|
|
53
|
+
const content = 'Test content';
|
|
54
|
+
|
|
55
|
+
service.writeToFile(filePath, content);
|
|
56
|
+
|
|
57
|
+
expect(fs.existsSync(filePath)).toBe(true);
|
|
58
|
+
const writtenContent = fs.readFileSync(filePath, 'utf8');
|
|
59
|
+
expect(writtenContent).toBe(content);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should overwrite existing file', () => {
|
|
63
|
+
const filePath = path.join(tempDir, 'test.txt');
|
|
64
|
+
|
|
65
|
+
service.writeToFile(filePath, 'First content');
|
|
66
|
+
service.writeToFile(filePath, 'Second content');
|
|
67
|
+
|
|
68
|
+
const writtenContent = fs.readFileSync(filePath, 'utf8');
|
|
69
|
+
expect(writtenContent).toBe('Second content');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('writeToStdout', () => {
|
|
74
|
+
let stdoutWriteSpy: ReturnType<typeof jest.spyOn>;
|
|
75
|
+
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
stdoutWriteSpy = jest
|
|
78
|
+
.spyOn(process.stdout, 'write')
|
|
79
|
+
.mockImplementation(() => true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
afterEach(() => {
|
|
83
|
+
stdoutWriteSpy.mockRestore();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should write content to stdout', () => {
|
|
87
|
+
const content = 'Test output';
|
|
88
|
+
|
|
89
|
+
service.writeToStdout(content);
|
|
90
|
+
|
|
91
|
+
expect(stdoutWriteSpy).toHaveBeenCalledWith(content);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should add newline if content does not end with one', () => {
|
|
95
|
+
const content = 'No newline';
|
|
96
|
+
|
|
97
|
+
service.writeToStdout(content);
|
|
98
|
+
|
|
99
|
+
expect(stdoutWriteSpy).toHaveBeenCalledWith(content);
|
|
100
|
+
expect(stdoutWriteSpy).toHaveBeenCalledWith('\n');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should not add extra newline if content already ends with one', () => {
|
|
104
|
+
const content = 'With newline\n';
|
|
105
|
+
|
|
106
|
+
service.writeToStdout(content);
|
|
107
|
+
|
|
108
|
+
expect(stdoutWriteSpy).toHaveBeenCalledWith(content);
|
|
109
|
+
expect(stdoutWriteSpy).toHaveBeenCalledTimes(1);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('write', () => {
|
|
114
|
+
it('should write to file when output path is provided', () => {
|
|
115
|
+
const filePath = path.join(tempDir, 'output.txt');
|
|
116
|
+
const content = 'File output';
|
|
117
|
+
|
|
118
|
+
service.write(content, filePath);
|
|
119
|
+
|
|
120
|
+
expect(fs.existsSync(filePath)).toBe(true);
|
|
121
|
+
const writtenContent = fs.readFileSync(filePath, 'utf8');
|
|
122
|
+
expect(writtenContent).toBe(content);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should write to stdout when output path is not provided', () => {
|
|
126
|
+
const stdoutWriteSpy = jest
|
|
127
|
+
.spyOn(process.stdout, 'write')
|
|
128
|
+
.mockImplementation(() => true);
|
|
129
|
+
|
|
130
|
+
const content = 'Stdout output';
|
|
131
|
+
|
|
132
|
+
service.write(content);
|
|
133
|
+
|
|
134
|
+
expect(stdoutWriteSpy).toHaveBeenCalledWith(content);
|
|
135
|
+
|
|
136
|
+
stdoutWriteSpy.mockRestore();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class FileWriterService {
|
|
7
|
+
writeToFile(filePath: string, content: string): void {
|
|
8
|
+
const absolutePath = path.resolve(filePath);
|
|
9
|
+
const directory = path.dirname(absolutePath);
|
|
10
|
+
|
|
11
|
+
if (!fs.existsSync(directory)) {
|
|
12
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fs.writeFileSync(absolutePath, content, 'utf8');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
writeToStdout(content: string): void {
|
|
19
|
+
process.stdout.write(content);
|
|
20
|
+
if (!content.endsWith('\n')) {
|
|
21
|
+
process.stdout.write('\n');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
write(content: string, outputPath?: string): void {
|
|
26
|
+
if (outputPath) {
|
|
27
|
+
this.writeToFile(outputPath, content);
|
|
28
|
+
} else {
|
|
29
|
+
this.writeToStdout(content);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
2
|
+
import { SchemaExportService } from './schema-export.service.js';
|
|
3
|
+
import { describe, beforeEach, it, expect } from '@jest/globals';
|
|
4
|
+
import { ConfigBound } from '@config-bound/config-bound';
|
|
5
|
+
import { Section } from '@config-bound/config-bound/section';
|
|
6
|
+
import { Element } from '@config-bound/config-bound/element';
|
|
7
|
+
import Joi from 'joi';
|
|
8
|
+
|
|
9
|
+
describe('SchemaExportService', () => {
|
|
10
|
+
let service: SchemaExportService;
|
|
11
|
+
let mockConfigInstance: ConfigBound;
|
|
12
|
+
let mockSections: Section[];
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
16
|
+
providers: [SchemaExportService]
|
|
17
|
+
}).compile();
|
|
18
|
+
|
|
19
|
+
service = module.get<SchemaExportService>(SchemaExportService);
|
|
20
|
+
|
|
21
|
+
const hostElement = new Element<string>(
|
|
22
|
+
'host',
|
|
23
|
+
'Database host',
|
|
24
|
+
'localhost',
|
|
25
|
+
undefined,
|
|
26
|
+
false,
|
|
27
|
+
false,
|
|
28
|
+
Joi.string()
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const portElement = new Element<number>(
|
|
32
|
+
'port',
|
|
33
|
+
'Database port',
|
|
34
|
+
5432,
|
|
35
|
+
undefined,
|
|
36
|
+
false,
|
|
37
|
+
false,
|
|
38
|
+
Joi.number()
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const databaseSection = new Section(
|
|
42
|
+
'database',
|
|
43
|
+
[hostElement, portElement],
|
|
44
|
+
'Database configuration'
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
mockSections = [databaseSection];
|
|
48
|
+
|
|
49
|
+
mockConfigInstance = new ConfigBound('TestConfig', [], [], undefined);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should be defined', () => {
|
|
53
|
+
expect(service).toBeDefined();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('exportToString', () => {
|
|
57
|
+
it('should export to JSON format', () => {
|
|
58
|
+
const result = service.exportToString(
|
|
59
|
+
'TestConfig',
|
|
60
|
+
mockSections,
|
|
61
|
+
mockConfigInstance,
|
|
62
|
+
{
|
|
63
|
+
format: 'json',
|
|
64
|
+
pretty: true
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(result).toContain('"name": "TestConfig"');
|
|
69
|
+
expect(result).toContain('"database"');
|
|
70
|
+
const parsed = JSON.parse(result);
|
|
71
|
+
expect(parsed.name).toBe('TestConfig');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should export to YAML format', () => {
|
|
75
|
+
const result = service.exportToString(
|
|
76
|
+
'TestConfig',
|
|
77
|
+
mockSections,
|
|
78
|
+
mockConfigInstance,
|
|
79
|
+
{
|
|
80
|
+
format: 'yaml'
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(result).toContain('name: TestConfig');
|
|
85
|
+
expect(result).toContain('- name: database');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should export to env format', () => {
|
|
89
|
+
const result = service.exportToString(
|
|
90
|
+
'TestConfig',
|
|
91
|
+
mockSections,
|
|
92
|
+
mockConfigInstance,
|
|
93
|
+
{
|
|
94
|
+
format: 'env'
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(result).toContain('DATABASE_HOST=');
|
|
99
|
+
expect(result).toContain('DATABASE_PORT=');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should throw error for unsupported format', () => {
|
|
103
|
+
expect(() =>
|
|
104
|
+
service.exportToString('TestConfig', mockSections, mockConfigInstance, {
|
|
105
|
+
format: 'xml' as any
|
|
106
|
+
})
|
|
107
|
+
).toThrow('Unsupported format: xml');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('getFileExtension', () => {
|
|
112
|
+
it('should return correct extension for json', () => {
|
|
113
|
+
expect(service.getFileExtension('json')).toBe('.json');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should return correct extension for yaml', () => {
|
|
117
|
+
expect(service.getFileExtension('yaml')).toBe('.yaml');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should return correct extension for env', () => {
|
|
121
|
+
expect(service.getFileExtension('env')).toBe('.env.example');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import {
|
|
3
|
+
exportSchema,
|
|
4
|
+
formatAsJSON,
|
|
5
|
+
formatAsYAML,
|
|
6
|
+
formatAsEnvExample
|
|
7
|
+
} from '@config-bound/schema-export';
|
|
8
|
+
import {
|
|
9
|
+
ConfigBound,
|
|
10
|
+
TypedConfigBound,
|
|
11
|
+
ConfigSchema
|
|
12
|
+
} from '@config-bound/config-bound';
|
|
13
|
+
import { Section } from '@config-bound/config-bound/section';
|
|
14
|
+
import { EnvVarBind } from '@config-bound/config-bound/bind/binds/envVar';
|
|
15
|
+
|
|
16
|
+
export type ExportFormat = 'json' | 'yaml' | 'env';
|
|
17
|
+
|
|
18
|
+
export interface ExportSchemaOptions {
|
|
19
|
+
format: ExportFormat;
|
|
20
|
+
includeOmitted?: boolean;
|
|
21
|
+
pretty?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Injectable()
|
|
25
|
+
export class SchemaExportService {
|
|
26
|
+
exportToString(
|
|
27
|
+
configName: string,
|
|
28
|
+
sections: Section[],
|
|
29
|
+
configInstance: ConfigBound | TypedConfigBound<ConfigSchema>,
|
|
30
|
+
options: ExportSchemaOptions
|
|
31
|
+
): string {
|
|
32
|
+
const includeOmitted = options.includeOmitted || false;
|
|
33
|
+
|
|
34
|
+
const schema = exportSchema(configName, sections, includeOmitted);
|
|
35
|
+
|
|
36
|
+
switch (options.format) {
|
|
37
|
+
case 'json':
|
|
38
|
+
return formatAsJSON(schema, options.pretty !== false);
|
|
39
|
+
|
|
40
|
+
case 'yaml':
|
|
41
|
+
return formatAsYAML(schema);
|
|
42
|
+
|
|
43
|
+
case 'env': {
|
|
44
|
+
// Extract prefix from EnvVarBind if present
|
|
45
|
+
const prefix = this.extractEnvVarPrefix(configInstance);
|
|
46
|
+
return formatAsEnvExample(schema, prefix);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
default:
|
|
50
|
+
throw new Error(`Unsupported format: ${options.format}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private extractEnvVarPrefix(
|
|
55
|
+
configInstance: ConfigBound | TypedConfigBound<ConfigSchema>
|
|
56
|
+
): string | undefined {
|
|
57
|
+
if (!configInstance || !configInstance.binds) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const bind of configInstance.binds) {
|
|
62
|
+
if (bind instanceof EnvVarBind) {
|
|
63
|
+
const envVarBind = bind as EnvVarBind;
|
|
64
|
+
if (envVarBind.envVarPrefix) {
|
|
65
|
+
return envVarBind.envVarPrefix;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getFileExtension(format: ExportFormat): string {
|
|
74
|
+
switch (format) {
|
|
75
|
+
case 'json':
|
|
76
|
+
return '.json';
|
|
77
|
+
case 'yaml':
|
|
78
|
+
return '.yaml';
|
|
79
|
+
case 'env':
|
|
80
|
+
return '.env.example';
|
|
81
|
+
default:
|
|
82
|
+
return '';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
package/tsconfig.json
ADDED