@config-bound/cli 0.1.0 → 0.2.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/CHANGELOG.md +16 -0
- package/package.json +6 -4
- package/scripts/generate-docs.ts +362 -0
- package/src/cli.module.ts +7 -1
- package/src/commands/generate-bind.command.ts +175 -0
- package/src/commands/generate.command.ts +13 -0
- package/src/services/bind-generator.service.ts +248 -0
- package/src/services/schema-export.service.spec.ts +3 -3
- package/tsconfig.json +7 -2
- package/.turbo/turbo-build.log +0 -4
- package/.turbo/turbo-format$colon$ci.log +0 -6
- package/.turbo/turbo-lint$colon$ci.log +0 -4
- package/.turbo/turbo-test.log +0 -19
- package/dist/cli.module.d.ts +0 -3
- package/dist/cli.module.d.ts.map +0 -1
- package/dist/cli.module.js +0 -29
- package/dist/cli.module.js.map +0 -1
- package/dist/commands/export.command.d.ts +0 -30
- package/dist/commands/export.command.d.ts.map +0 -1
- package/dist/commands/export.command.js +0 -226
- package/dist/commands/export.command.js.map +0 -1
- package/dist/commands/list.command.d.ts +0 -13
- package/dist/commands/list.command.d.ts.map +0 -1
- package/dist/commands/list.command.js +0 -93
- package/dist/commands/list.command.js.map +0 -1
- package/dist/main.d.ts +0 -3
- package/dist/main.d.ts.map +0 -1
- package/dist/main.js +0 -17
- package/dist/main.js.map +0 -1
- package/dist/services/config-discovery.service.d.ts +0 -15
- package/dist/services/config-discovery.service.d.ts.map +0 -1
- package/dist/services/config-discovery.service.js +0 -191
- package/dist/services/config-discovery.service.js.map +0 -1
- package/dist/services/config-discovery.service.spec.d.ts +0 -2
- package/dist/services/config-discovery.service.spec.d.ts.map +0 -1
- package/dist/services/config-discovery.service.spec.js +0 -137
- package/dist/services/config-discovery.service.spec.js.map +0 -1
- package/dist/services/config-loader.service.d.ts +0 -13
- package/dist/services/config-loader.service.d.ts.map +0 -1
- package/dist/services/config-loader.service.js +0 -241
- package/dist/services/config-loader.service.js.map +0 -1
- package/dist/services/file-writer.service.d.ts +0 -6
- package/dist/services/file-writer.service.d.ts.map +0 -1
- package/dist/services/file-writer.service.js +0 -38
- package/dist/services/file-writer.service.js.map +0 -1
- package/dist/services/file-writer.service.spec.d.ts +0 -2
- package/dist/services/file-writer.service.spec.d.ts.map +0 -1
- package/dist/services/file-writer.service.spec.js +0 -98
- package/dist/services/file-writer.service.spec.js.map +0 -1
- package/dist/services/schema-export.service.d.ts +0 -14
- package/dist/services/schema-export.service.d.ts.map +0 -1
- package/dist/services/schema-export.service.js +0 -58
- package/dist/services/schema-export.service.js.map +0 -1
- package/dist/services/schema-export.service.spec.d.ts +0 -2
- package/dist/services/schema-export.service.spec.d.ts.map +0 -1
- package/dist/services/schema-export.service.spec.js +0 -69
- package/dist/services/schema-export.service.spec.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @config-bound/cli
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f2299f6: Add `configbound generate bind` command
|
|
8
|
+
|
|
9
|
+
Scaffolds the boilerplate for a new custom bind — either as an embedded TypeScript class or a publishable npm package. Handles class structure, static `create()` factory, and cache setup so you can focus on the implementation.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [02b3369]
|
|
14
|
+
- Updated dependencies [ac54dea]
|
|
15
|
+
- Updated dependencies [0f899ee]
|
|
16
|
+
- @config-bound/config-bound@0.2.0
|
|
17
|
+
- @config-bound/schema-export@0.1.1
|
|
18
|
+
|
|
3
19
|
## 0.1.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@config-bound/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "CLI tool for ConfigBound schema export and management",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@config-bound/config-bound": "*",
|
|
34
34
|
"@config-bound/schema-export": "*",
|
|
35
|
-
"@nestjs/common": "^
|
|
36
|
-
"@nestjs/core": "^
|
|
35
|
+
"@nestjs/common": "^11.1.18",
|
|
36
|
+
"@nestjs/core": "^11.1.18",
|
|
37
37
|
"chalk": "^4.1.2",
|
|
38
38
|
"esbuild-register": "^3.6.0",
|
|
39
39
|
"nest-commander": "^3.15.0",
|
|
@@ -43,13 +43,15 @@
|
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@config-bound/eslint-config": "*",
|
|
46
|
+
"@nestjs/testing": "^11.1.18",
|
|
46
47
|
"@types/jest": "^30.0.0",
|
|
47
48
|
"@types/node": "^24.10.1",
|
|
48
49
|
"eslint": "^9.39.1",
|
|
49
50
|
"jest": "^30.2.0",
|
|
51
|
+
"markdown-table": "^3.0.4",
|
|
50
52
|
"rimraf": "^6.1.2",
|
|
51
53
|
"ts-jest": "^29.4.5",
|
|
52
|
-
"typescript": "^
|
|
54
|
+
"typescript": "^6.0.0"
|
|
53
55
|
},
|
|
54
56
|
"publishConfig": {
|
|
55
57
|
"access": "public",
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates CLI reference documentation for the docs site.
|
|
3
|
+
*
|
|
4
|
+
* WHAT IT DOES:
|
|
5
|
+
* Boots the Nest CLI application, extracts command metadata from
|
|
6
|
+
* registered CommandRunner instances, and generates markdown reference pages
|
|
7
|
+
* combining authored prose with derived options tables.
|
|
8
|
+
*
|
|
9
|
+
* DEPENDENCIES:
|
|
10
|
+
* - @config-bound/cli package with Nest application and command runners
|
|
11
|
+
* - apps/cli/src/cli.module.ts and command classes
|
|
12
|
+
*
|
|
13
|
+
* OUTPUT:
|
|
14
|
+
* - apps/docs/reference/cli/*.md (index, list, export, generate-bind)
|
|
15
|
+
*
|
|
16
|
+
* RUN VIA:
|
|
17
|
+
* npm run docs:cli (from repo root)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import 'reflect-metadata';
|
|
21
|
+
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
22
|
+
import { resolve } from 'path';
|
|
23
|
+
import { markdownTable } from 'markdown-table';
|
|
24
|
+
import { CommandFactory } from 'nest-commander';
|
|
25
|
+
import { CliModule } from '../src/cli.module.js';
|
|
26
|
+
import { ExportCommand } from '../src/commands/export.command.js';
|
|
27
|
+
import { ListCommand } from '../src/commands/list.command.js';
|
|
28
|
+
import { GenerateBindCommand } from '../src/commands/generate-bind.command.js';
|
|
29
|
+
|
|
30
|
+
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
interface CommanderOption {
|
|
33
|
+
flags: string;
|
|
34
|
+
description: string;
|
|
35
|
+
defaultValue: unknown;
|
|
36
|
+
hidden: boolean;
|
|
37
|
+
long: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface CommandRunner {
|
|
41
|
+
command: { options: CommanderOption[] };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A page definition. `sections` is authored prose; `optionsHeading` marks where
|
|
46
|
+
* the derived options table is injected. Pages without options omit the heading.
|
|
47
|
+
*/
|
|
48
|
+
interface PageSpec {
|
|
49
|
+
filename: string;
|
|
50
|
+
frontmatterDescription: string;
|
|
51
|
+
title: string;
|
|
52
|
+
intro: string;
|
|
53
|
+
usage: string;
|
|
54
|
+
positionalArgs?: { name: string; description: string; default?: string }[];
|
|
55
|
+
/** Heading text under which the derived options table is rendered. */
|
|
56
|
+
optionsHeading: string;
|
|
57
|
+
sections: { heading: string; body: string }[];
|
|
58
|
+
related: { text: string; link: string }[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── Rendering ─────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
const SCRIPT_NAME = 'generate-docs';
|
|
64
|
+
const GENERATED_NOTICE = `<!-- This file is generated by scripts/generate-cli-docs.mjs. Do not edit it directly. -->`;
|
|
65
|
+
|
|
66
|
+
function renderOptionsTable(options: CommanderOption[]): string {
|
|
67
|
+
const visible = options.filter(
|
|
68
|
+
(opt) => !opt.hidden && opt.long !== '--help' && opt.long !== '--version',
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const rows = visible.map((opt) => [
|
|
72
|
+
`\`${opt.flags}\``,
|
|
73
|
+
opt.description,
|
|
74
|
+
opt.defaultValue !== undefined && opt.defaultValue !== null
|
|
75
|
+
? `\`${String(opt.defaultValue)}\``
|
|
76
|
+
: '—'
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
return markdownTable([
|
|
80
|
+
['Flag', 'Description', 'Default'],
|
|
81
|
+
...rows
|
|
82
|
+
]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderPage(spec: PageSpec, runner: CommandRunner | null): string {
|
|
86
|
+
const parts: string[] = [
|
|
87
|
+
`---\ndescription: ${spec.frontmatterDescription}\n---`,
|
|
88
|
+
GENERATED_NOTICE,
|
|
89
|
+
`# ${spec.title}`,
|
|
90
|
+
spec.intro,
|
|
91
|
+
`## Usage\n\n\`\`\`text\n${spec.usage}\n\`\`\``,
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
if (spec.positionalArgs && spec.positionalArgs.length > 0) {
|
|
95
|
+
const rows = spec.positionalArgs.map((a) => [
|
|
96
|
+
`\`${a.name}\``,
|
|
97
|
+
a.description,
|
|
98
|
+
a.default ?? 'Required'
|
|
99
|
+
]);
|
|
100
|
+
const argsTable = markdownTable([
|
|
101
|
+
['Argument', 'Description', 'Default'],
|
|
102
|
+
...rows
|
|
103
|
+
]);
|
|
104
|
+
parts.push(`### Arguments\n\n${argsTable}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (runner) {
|
|
108
|
+
parts.push(`### ${spec.optionsHeading}\n\n${renderOptionsTable(runner.command.options)}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const section of spec.sections) {
|
|
112
|
+
parts.push(`## ${section.heading}\n\n${section.body}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (spec.related.length > 0) {
|
|
116
|
+
const links = spec.related.map((r) => `- [${r.text}](${r.link})`).join('\n');
|
|
117
|
+
parts.push(`## Related\n\n${links}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return parts.join('\n\n');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── Page definitions ───────────────────────────────────────────────────────
|
|
124
|
+
// Authored prose lives here. Derived content (options tables) is injected at
|
|
125
|
+
// render time from the live CommandRunner instances.
|
|
126
|
+
|
|
127
|
+
const INDEX_PAGE = `---
|
|
128
|
+
description: The ConfigBound CLI provides tools to discover, inspect, and export configuration schemas without writing code.
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
${GENERATED_NOTICE}
|
|
132
|
+
|
|
133
|
+
# CLI
|
|
134
|
+
|
|
135
|
+
The \`@config-bound/cli\` package provides the \`configbound\` command — a set of tools for working with ConfigBound configurations from the terminal.
|
|
136
|
+
|
|
137
|
+
## Installation
|
|
138
|
+
|
|
139
|
+
\`\`\`bash
|
|
140
|
+
npm install --save-dev @config-bound/cli
|
|
141
|
+
\`\`\`
|
|
142
|
+
|
|
143
|
+
After installation, \`npx configbound\` runs the CLI. For global use:
|
|
144
|
+
|
|
145
|
+
\`\`\`bash
|
|
146
|
+
npm install --global @config-bound/cli
|
|
147
|
+
\`\`\`
|
|
148
|
+
|
|
149
|
+
## Commands
|
|
150
|
+
|
|
151
|
+
| Command | Description |
|
|
152
|
+
| ------- | ----------- |
|
|
153
|
+
| [\`configbound list\`](./list) | Discover and display all ConfigBound configurations in a directory tree |
|
|
154
|
+
| [\`configbound export\`](./export) | Export a configuration schema to JSON, YAML, or \`.env\` format |
|
|
155
|
+
| [\`configbound generate bind\`](./generate-bind) | Scaffold a new bind — embedded class or publishable package |
|
|
156
|
+
|
|
157
|
+
## How discovery works
|
|
158
|
+
|
|
159
|
+
\`list\` and \`export\` both use config discovery — scanning source files for ConfigBound configurations. Discovery reads the actual exports from your TypeScript files. No separate manifest or registration step is required.
|
|
160
|
+
|
|
161
|
+
\`export\` auto-selects a config when only one is found. When multiple configs exist in the same project, it prompts for selection. Pass \`--config\` and \`--name\` explicitly to skip discovery entirely.
|
|
162
|
+
|
|
163
|
+
## CLI vs. library
|
|
164
|
+
|
|
165
|
+
The CLI is a companion to the library, not a replacement for it. Schema definition, validation, and value resolution all happen in code. The CLI's job is to inspect what the library produces — listing what configs exist and exporting their schemas as artifacts.`;
|
|
166
|
+
|
|
167
|
+
const LIST_SPEC: PageSpec = {
|
|
168
|
+
filename: 'list.md',
|
|
169
|
+
frontmatterDescription:
|
|
170
|
+
'Discover and display all ConfigBound configurations in a project using configbound list.',
|
|
171
|
+
title: 'configbound list',
|
|
172
|
+
intro:
|
|
173
|
+
'`list` scans a directory tree for ConfigBound configurations and prints what it finds, grouped by file.\n\nUse it to confirm that discovery works correctly, or to get the exact file paths and export names needed for `configbound export --config ... --name ...`.',
|
|
174
|
+
usage: 'configbound list [path] [options]',
|
|
175
|
+
positionalArgs: [{ name: 'path', description: 'Directory to search', default: 'Current working directory' }],
|
|
176
|
+
optionsHeading: 'Options',
|
|
177
|
+
sections: [
|
|
178
|
+
{
|
|
179
|
+
heading: 'Examples',
|
|
180
|
+
body: `\`\`\`bash
|
|
181
|
+
# List all configs in the current project
|
|
182
|
+
configbound list
|
|
183
|
+
|
|
184
|
+
# Search a specific directory
|
|
185
|
+
configbound list ./src
|
|
186
|
+
|
|
187
|
+
# Search non-recursively
|
|
188
|
+
configbound list --recursive false
|
|
189
|
+
\`\`\``,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
heading: 'Output',
|
|
193
|
+
body: `Results are grouped by file. Each entry shows the export name, whether it is a default or named export, the config's display name if set, and the line number:
|
|
194
|
+
|
|
195
|
+
\`\`\`text
|
|
196
|
+
📄 src/config/app.config.ts
|
|
197
|
+
default (default) - My App Config :42
|
|
198
|
+
|
|
199
|
+
📄 src/config/db.config.ts
|
|
200
|
+
default (default) - Database Config :18
|
|
201
|
+
dbConfig (named) - Database Config (read-only) :67
|
|
202
|
+
\`\`\`
|
|
203
|
+
|
|
204
|
+
When a file has multiple exports, each is listed separately. Use the export name with \`--name\` when running \`export\`:
|
|
205
|
+
|
|
206
|
+
\`\`\`bash
|
|
207
|
+
configbound export --config src/config/db.config.ts --name dbConfig
|
|
208
|
+
\`\`\``,
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
related: [{ text: '`configbound export`', link: './export' }],
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const EXPORT_SPEC: PageSpec = {
|
|
215
|
+
filename: 'export.md',
|
|
216
|
+
frontmatterDescription:
|
|
217
|
+
'Export a ConfigBound configuration schema to JSON, YAML, or .env format using configbound export.',
|
|
218
|
+
title: 'configbound export',
|
|
219
|
+
intro:
|
|
220
|
+
'`export` loads a ConfigBound configuration and serializes its schema to JSON, YAML, or `.env` format. The output goes to stdout by default, or to a file with `--output`.\n\nThis is the CLI surface for schema export. For the programmatic equivalent, see [Export your configuration schema](/how-to/schema-export). For why schema export matters, see [Schema as the Source of Truth](/explanation/schema-source-of-truth).',
|
|
221
|
+
usage: 'configbound export [options]',
|
|
222
|
+
optionsHeading: 'Options',
|
|
223
|
+
sections: [
|
|
224
|
+
{
|
|
225
|
+
heading: 'Discovery',
|
|
226
|
+
body: `When \`--config\` is omitted, \`export\` scans the current directory for ConfigBound configurations:
|
|
227
|
+
|
|
228
|
+
- One config found: selected automatically.
|
|
229
|
+
- Multiple configs found: a numbered menu is displayed and you choose.
|
|
230
|
+
- No configs found: the command exits and suggests using \`--config\`.
|
|
231
|
+
|
|
232
|
+
Pass \`--config\` to skip discovery entirely. When the file has more than one export, also pass \`--name\` to identify which one to export.`,
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
heading: 'Examples',
|
|
236
|
+
body: `\`\`\`bash
|
|
237
|
+
# Auto-discover and export to stdout as JSON
|
|
238
|
+
configbound export
|
|
239
|
+
|
|
240
|
+
# Export a specific file as YAML
|
|
241
|
+
configbound export --config ./src/config/app.config.ts --format yaml
|
|
242
|
+
|
|
243
|
+
# Write JSON output to a file
|
|
244
|
+
configbound export --output ./docs/schema.json
|
|
245
|
+
|
|
246
|
+
# Named export from a file with multiple configs
|
|
247
|
+
configbound export --config ./src/config/all.ts --name databaseConfig
|
|
248
|
+
|
|
249
|
+
# Generate a .env file including normally-omitted elements
|
|
250
|
+
configbound export --format env --include-omitted --output .env.example
|
|
251
|
+
\`\`\``,
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
heading: 'Omitted elements',
|
|
255
|
+
body: 'Elements defined with `omitFromSchema: true` are excluded from exported output by default. The intent is to keep sensitive or internal fields out of generated artifacts. Pass `--include-omitted` to override this — useful when generating a complete `.env.example` for onboarding or local development.',
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
related: [
|
|
259
|
+
{ text: '`configbound list`', link: './list' },
|
|
260
|
+
{ text: 'Export your configuration schema', link: '/how-to/schema-export' },
|
|
261
|
+
{ text: 'Schema as the Source of Truth', link: '/explanation/schema-source-of-truth' },
|
|
262
|
+
],
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const GENERATE_BIND_SPEC: PageSpec = {
|
|
266
|
+
filename: 'generate-bind.md',
|
|
267
|
+
frontmatterDescription: 'Scaffold a new custom bind using configbound generate bind.',
|
|
268
|
+
title: 'configbound generate bind',
|
|
269
|
+
intro:
|
|
270
|
+
'`generate bind` scaffolds the boilerplate for a new bind. It handles the repetitive parts — class structure, factory pattern, cache setup — so you can focus on the implementation.',
|
|
271
|
+
usage: 'configbound generate bind <name> [options]',
|
|
272
|
+
positionalArgs: [
|
|
273
|
+
{ name: 'name', description: 'The bind name in kebab-case (e.g. `vault`, `aws-ssm`, `1password`)', default: 'Required' },
|
|
274
|
+
],
|
|
275
|
+
optionsHeading: 'Options',
|
|
276
|
+
sections: [
|
|
277
|
+
{
|
|
278
|
+
heading: 'Generation modes',
|
|
279
|
+
body: `**\`embedded\`** generates a single TypeScript class file. The bind lives in your project and is not published. Use this when the bind is specific to one application.
|
|
280
|
+
|
|
281
|
+
**\`package\`** generates a full npm package scaffold: \`package.json\`, \`tsconfig.json\`, \`eslint.config.mjs\`, and a \`src/\` directory containing the bind class and an index. Use this when you intend to publish the bind for others to use.`,
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
heading: 'Examples',
|
|
285
|
+
body: `\`\`\`bash
|
|
286
|
+
# Interactive — prompts for embedded or package
|
|
287
|
+
configbound generate bind vault
|
|
288
|
+
|
|
289
|
+
# Non-interactive embedded bind
|
|
290
|
+
configbound generate bind vault --type embedded
|
|
291
|
+
|
|
292
|
+
# Non-interactive package scaffold
|
|
293
|
+
configbound generate bind aws-ssm --type package
|
|
294
|
+
|
|
295
|
+
# Preview what would be generated without writing anything
|
|
296
|
+
configbound generate bind 1password --type package --dry-run
|
|
297
|
+
\`\`\``,
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
heading: 'What gets generated',
|
|
301
|
+
body: `Both modes produce a class that extends \`Bind\` with a static async \`create()\` factory and a synchronous \`retrieve()\` method. The generated code includes inline comments explaining what to fill in.
|
|
302
|
+
|
|
303
|
+
The factory pattern pre-loads values into a cache at startup. \`retrieve()\` reads from that cache synchronously. This satisfies the \`Bind\` contract without requiring changes to the core library.`,
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
heading: 'Name derivation',
|
|
307
|
+
body: 'The kebab-case name you provide is used to derive the class name. `vault` becomes `VaultBind`. `aws-ssm` becomes `AwsSsmBind`. Leading digits are replaced with words: `1password` becomes `OnepasswordBind`.',
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
related: [
|
|
311
|
+
{ text: 'Create a custom bind', link: '/how-to/custom-bind' },
|
|
312
|
+
{
|
|
313
|
+
text: '`Bind` API reference',
|
|
314
|
+
link: '/reference/api/@config-bound.config-bound.bind.bind.Class.Bind',
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// ─── Main ──────────────────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
function writeDocFile(outDir: string, filename: string, content: string): void {
|
|
322
|
+
writeFileSync(resolve(outDir, filename), content + '\n', 'utf-8');
|
|
323
|
+
console.log(` generated: reference/cli/${filename}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function main(): Promise<void> {
|
|
327
|
+
// Optional output directory override (defaults to apps/docs/reference/cli).
|
|
328
|
+
// Used for custom builds or testing. Normal builds use the default.
|
|
329
|
+
const outDir = process.argv[2] ?? resolve(import.meta.dirname, '../../apps/docs/reference/cli');
|
|
330
|
+
|
|
331
|
+
const app = await CommandFactory.createWithoutRunning(CliModule, { logger: false });
|
|
332
|
+
|
|
333
|
+
const listRunner = app.get(ListCommand) as CommandRunner;
|
|
334
|
+
const exportRunner = app.get(ExportCommand) as CommandRunner;
|
|
335
|
+
const generateBindRunner = app.get(GenerateBindCommand) as CommandRunner;
|
|
336
|
+
|
|
337
|
+
if (!listRunner?.command?.options) {
|
|
338
|
+
throw new Error(`[${SCRIPT_NAME}] Failed to retrieve ListCommand with valid options`);
|
|
339
|
+
}
|
|
340
|
+
if (!exportRunner?.command?.options) {
|
|
341
|
+
throw new Error(`[${SCRIPT_NAME}] Failed to retrieve ExportCommand with valid options`);
|
|
342
|
+
}
|
|
343
|
+
if (!generateBindRunner?.command?.options) {
|
|
344
|
+
throw new Error(`[${SCRIPT_NAME}] Failed to retrieve GenerateBindCommand with valid options`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
348
|
+
|
|
349
|
+
console.log(`[${SCRIPT_NAME}] Generating CLI reference docs...`);
|
|
350
|
+
writeDocFile(outDir, 'index.md', INDEX_PAGE);
|
|
351
|
+
writeDocFile(outDir, LIST_SPEC.filename, renderPage(LIST_SPEC, listRunner));
|
|
352
|
+
writeDocFile(outDir, EXPORT_SPEC.filename, renderPage(EXPORT_SPEC, exportRunner));
|
|
353
|
+
writeDocFile(outDir, GENERATE_BIND_SPEC.filename, renderPage(GENERATE_BIND_SPEC, generateBindRunner));
|
|
354
|
+
console.log(`[${SCRIPT_NAME}] Done.`);
|
|
355
|
+
|
|
356
|
+
await app.close();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
main().catch((err: unknown) => {
|
|
360
|
+
console.error(`[${SCRIPT_NAME}] Unexpected error:`, err);
|
|
361
|
+
process.exit(1);
|
|
362
|
+
});
|
package/src/cli.module.ts
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
import { Module } from '@nestjs/common';
|
|
2
2
|
import { ListCommand } from './commands/list.command.js';
|
|
3
3
|
import { ExportCommand } from './commands/export.command.js';
|
|
4
|
+
import { GenerateCommand } from './commands/generate.command.js';
|
|
5
|
+
import { GenerateBindCommand } from './commands/generate-bind.command.js';
|
|
4
6
|
import { ConfigDiscoveryService } from './services/config-discovery.service.js';
|
|
5
7
|
import { ConfigLoaderService } from './services/config-loader.service.js';
|
|
6
8
|
import { SchemaExportService } from './services/schema-export.service.js';
|
|
7
9
|
import { FileWriterService } from './services/file-writer.service.js';
|
|
10
|
+
import { BindGeneratorService } from './services/bind-generator.service.js';
|
|
8
11
|
|
|
9
12
|
@Module({
|
|
10
13
|
providers: [
|
|
11
14
|
ListCommand,
|
|
12
15
|
ExportCommand,
|
|
16
|
+
GenerateCommand,
|
|
17
|
+
GenerateBindCommand,
|
|
13
18
|
ConfigDiscoveryService,
|
|
14
19
|
ConfigLoaderService,
|
|
15
20
|
SchemaExportService,
|
|
16
|
-
FileWriterService
|
|
21
|
+
FileWriterService,
|
|
22
|
+
BindGeneratorService
|
|
17
23
|
]
|
|
18
24
|
})
|
|
19
25
|
export class CliModule {}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { Command, CommandRunner, Option } from 'nest-commander';
|
|
2
|
+
import {
|
|
3
|
+
BindGeneratorService,
|
|
4
|
+
type BindGeneratorMode,
|
|
5
|
+
type BindNames
|
|
6
|
+
} from '../services/bind-generator.service.js';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import * as readline from 'readline';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
|
|
11
|
+
interface GenerateBindCommandOptions {
|
|
12
|
+
output?: string;
|
|
13
|
+
type?: BindGeneratorMode;
|
|
14
|
+
dryRun?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@Command({
|
|
18
|
+
name: 'bind',
|
|
19
|
+
description: 'Scaffold a new bind',
|
|
20
|
+
arguments: '<name>',
|
|
21
|
+
argsDescription: {
|
|
22
|
+
name: 'The bind name in kebab-case (e.g. 1password, vault, aws-ssm)'
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
export class GenerateBindCommand extends CommandRunner {
|
|
26
|
+
constructor(private readonly generatorService: BindGeneratorService) {
|
|
27
|
+
super();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async run(
|
|
31
|
+
[name]: string[],
|
|
32
|
+
options?: GenerateBindCommandOptions
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
if (!name) {
|
|
35
|
+
this.command.error(
|
|
36
|
+
'bind name is required.\nUsage: configbound generate bind <name>'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const names = this.generatorService.deriveNames(name);
|
|
41
|
+
const outputDir = options?.output
|
|
42
|
+
? path.resolve(options.output)
|
|
43
|
+
: process.cwd();
|
|
44
|
+
|
|
45
|
+
const mode: BindGeneratorMode = options?.type ?? (await this.promptMode());
|
|
46
|
+
|
|
47
|
+
const files = this.generatorService.renderFiles(names, mode);
|
|
48
|
+
|
|
49
|
+
if (options?.dryRun) {
|
|
50
|
+
console.log(chalk.blue('\nFiles that would be generated:\n'));
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
const fullPath = path.join(outputDir, file.relativePath);
|
|
53
|
+
console.log(chalk.cyan(` ${fullPath}`));
|
|
54
|
+
console.log(chalk.gray(' ' + '─'.repeat(60)));
|
|
55
|
+
console.log(
|
|
56
|
+
file.content
|
|
57
|
+
.split('\n')
|
|
58
|
+
.map((line) => chalk.gray(` ${line}`))
|
|
59
|
+
.join('\n')
|
|
60
|
+
);
|
|
61
|
+
console.log();
|
|
62
|
+
}
|
|
63
|
+
console.log(chalk.yellow('Dry run — no files written.'));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(
|
|
68
|
+
chalk.blue(
|
|
69
|
+
`\nGenerating ${mode === 'package' ? 'community bind package' : 'embedded bind class'} for ${chalk.bold(names.pascal)}...\n`
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
this.generatorService.writeFiles(files, outputDir);
|
|
74
|
+
|
|
75
|
+
console.log(chalk.green('Done! Generated files:'));
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
console.log(chalk.cyan(` ${path.join(outputDir, file.relativePath)}`));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.printNextSteps(names, mode, outputDir);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private async promptMode(): Promise<BindGeneratorMode> {
|
|
84
|
+
console.log(chalk.blue('\nWhat would you like to generate?\n'));
|
|
85
|
+
console.log(
|
|
86
|
+
` ${chalk.cyan('1.')} Community bind package ${chalk.gray('(package.json, tsconfig, src/) — standalone, publishable to npm')}`
|
|
87
|
+
);
|
|
88
|
+
console.log(
|
|
89
|
+
` ${chalk.cyan('2.')} Embedded bind class file ${chalk.gray('— lives in your project, not published')}`
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
while (true) {
|
|
93
|
+
const answer = await new Promise<string>((resolve) => {
|
|
94
|
+
const rl = readline.createInterface({
|
|
95
|
+
input: process.stdin,
|
|
96
|
+
output: process.stdout
|
|
97
|
+
});
|
|
98
|
+
rl.question(chalk.yellow('\nSelect (1 or 2): '), (ans) => {
|
|
99
|
+
rl.close();
|
|
100
|
+
resolve(ans.trim());
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (answer === '1') return 'package';
|
|
105
|
+
if (answer === '2') return 'embedded';
|
|
106
|
+
|
|
107
|
+
console.log(chalk.red(' Invalid selection. Please enter 1 or 2.'));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private printNextSteps(
|
|
112
|
+
names: BindNames,
|
|
113
|
+
mode: BindGeneratorMode,
|
|
114
|
+
outputDir: string
|
|
115
|
+
): void {
|
|
116
|
+
console.log(chalk.blue('\nNext steps:\n'));
|
|
117
|
+
|
|
118
|
+
if (mode === 'package') {
|
|
119
|
+
const packageDir = path.join(outputDir, `bind-${names.kebab}`);
|
|
120
|
+
console.log(
|
|
121
|
+
` ${chalk.cyan('1.')} Add your SDK dependency to ${chalk.bold(path.join(packageDir, 'package.json'))}`
|
|
122
|
+
);
|
|
123
|
+
console.log(
|
|
124
|
+
` ${chalk.cyan('2.')} Fill in the ${chalk.bold(`create()`)} method in ${chalk.bold(path.join(packageDir, 'src', `${names.pascal}Bind.ts`))}`
|
|
125
|
+
);
|
|
126
|
+
console.log(
|
|
127
|
+
` ${chalk.cyan('3.')} Run ${chalk.bold('npm install')} in the package directory`
|
|
128
|
+
);
|
|
129
|
+
console.log(
|
|
130
|
+
` ${chalk.cyan('4.')} When ready to publish, set ${chalk.bold('"private": false')} in package.json`
|
|
131
|
+
);
|
|
132
|
+
} else {
|
|
133
|
+
const classFile = path.join(outputDir, `${names.pascal}Bind.ts`);
|
|
134
|
+
console.log(
|
|
135
|
+
` ${chalk.cyan('1.')} Add your SDK to your project's dependencies`
|
|
136
|
+
);
|
|
137
|
+
console.log(
|
|
138
|
+
` ${chalk.cyan('2.')} Fill in the ${chalk.bold(`create()`)} method in ${chalk.bold(classFile)}`
|
|
139
|
+
);
|
|
140
|
+
console.log(
|
|
141
|
+
` ${chalk.cyan('3.')} Pass an instance to ${chalk.bold('ConfigBound.createConfig(..., { binds: [bind] })')}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@Option({
|
|
149
|
+
flags: '-o, --output <dir>',
|
|
150
|
+
description: 'Output directory (default: current directory)'
|
|
151
|
+
})
|
|
152
|
+
parseOutput(val: string): string {
|
|
153
|
+
return val;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@Option({
|
|
157
|
+
flags: '--type <type>',
|
|
158
|
+
description:
|
|
159
|
+
'Generation mode: package or embedded (skips interactive prompt)'
|
|
160
|
+
})
|
|
161
|
+
parseType(val: string): BindGeneratorMode {
|
|
162
|
+
if (val !== 'package' && val !== 'embedded') {
|
|
163
|
+
throw new Error(`Invalid type: ${val}. Must be "package" or "embedded".`);
|
|
164
|
+
}
|
|
165
|
+
return val;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@Option({
|
|
169
|
+
flags: '--dry-run',
|
|
170
|
+
description: 'Preview generated files without writing'
|
|
171
|
+
})
|
|
172
|
+
parseDryRun(): boolean {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command, CommandRunner } from 'nest-commander';
|
|
2
|
+
import { GenerateBindCommand } from './generate-bind.command.js';
|
|
3
|
+
|
|
4
|
+
@Command({
|
|
5
|
+
name: 'generate',
|
|
6
|
+
description: 'Generate new ConfigBound components',
|
|
7
|
+
subCommands: [GenerateBindCommand]
|
|
8
|
+
})
|
|
9
|
+
export class GenerateCommand extends CommandRunner {
|
|
10
|
+
async run(): Promise<void> {
|
|
11
|
+
this.command.help();
|
|
12
|
+
}
|
|
13
|
+
}
|