@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +6 -4
  3. package/scripts/generate-docs.ts +362 -0
  4. package/src/cli.module.ts +7 -1
  5. package/src/commands/generate-bind.command.ts +175 -0
  6. package/src/commands/generate.command.ts +13 -0
  7. package/src/services/bind-generator.service.ts +248 -0
  8. package/src/services/schema-export.service.spec.ts +3 -3
  9. package/tsconfig.json +7 -2
  10. package/.turbo/turbo-build.log +0 -4
  11. package/.turbo/turbo-format$colon$ci.log +0 -6
  12. package/.turbo/turbo-lint$colon$ci.log +0 -4
  13. package/.turbo/turbo-test.log +0 -19
  14. package/dist/cli.module.d.ts +0 -3
  15. package/dist/cli.module.d.ts.map +0 -1
  16. package/dist/cli.module.js +0 -29
  17. package/dist/cli.module.js.map +0 -1
  18. package/dist/commands/export.command.d.ts +0 -30
  19. package/dist/commands/export.command.d.ts.map +0 -1
  20. package/dist/commands/export.command.js +0 -226
  21. package/dist/commands/export.command.js.map +0 -1
  22. package/dist/commands/list.command.d.ts +0 -13
  23. package/dist/commands/list.command.d.ts.map +0 -1
  24. package/dist/commands/list.command.js +0 -93
  25. package/dist/commands/list.command.js.map +0 -1
  26. package/dist/main.d.ts +0 -3
  27. package/dist/main.d.ts.map +0 -1
  28. package/dist/main.js +0 -17
  29. package/dist/main.js.map +0 -1
  30. package/dist/services/config-discovery.service.d.ts +0 -15
  31. package/dist/services/config-discovery.service.d.ts.map +0 -1
  32. package/dist/services/config-discovery.service.js +0 -191
  33. package/dist/services/config-discovery.service.js.map +0 -1
  34. package/dist/services/config-discovery.service.spec.d.ts +0 -2
  35. package/dist/services/config-discovery.service.spec.d.ts.map +0 -1
  36. package/dist/services/config-discovery.service.spec.js +0 -137
  37. package/dist/services/config-discovery.service.spec.js.map +0 -1
  38. package/dist/services/config-loader.service.d.ts +0 -13
  39. package/dist/services/config-loader.service.d.ts.map +0 -1
  40. package/dist/services/config-loader.service.js +0 -241
  41. package/dist/services/config-loader.service.js.map +0 -1
  42. package/dist/services/file-writer.service.d.ts +0 -6
  43. package/dist/services/file-writer.service.d.ts.map +0 -1
  44. package/dist/services/file-writer.service.js +0 -38
  45. package/dist/services/file-writer.service.js.map +0 -1
  46. package/dist/services/file-writer.service.spec.d.ts +0 -2
  47. package/dist/services/file-writer.service.spec.d.ts.map +0 -1
  48. package/dist/services/file-writer.service.spec.js +0 -98
  49. package/dist/services/file-writer.service.spec.js.map +0 -1
  50. package/dist/services/schema-export.service.d.ts +0 -14
  51. package/dist/services/schema-export.service.d.ts.map +0 -1
  52. package/dist/services/schema-export.service.js +0 -58
  53. package/dist/services/schema-export.service.js.map +0 -1
  54. package/dist/services/schema-export.service.spec.d.ts +0 -2
  55. package/dist/services/schema-export.service.spec.d.ts.map +0 -1
  56. package/dist/services/schema-export.service.spec.js +0 -69
  57. 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.1.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": "^10.4.15",
36
- "@nestjs/core": "^10.4.15",
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": "^5.9.3"
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
+ }