cdk8s-opinionated-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/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # cdk8s-opinionated-cli
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0fff51b: Initial release.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [0fff51b]
12
+ - cdk8s-kbld2@0.1.0
13
+ - cdk8s-local@0.1.0
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # cdk8s-opinionated-cli
2
+
3
+ **TLDR: A function to wrap your cdk8s app with a CLI interface that allows for passing custom command line arguments.**
4
+
5
+ Whenever I use cdk8s, I find myself writing a simple CLI tool that runs a cdk8s synth but allows me to pass custom command line arguments to modify the behaviour of my app, such as passing a config file or environment name. So this package provides a function to do just that, with minimal boilerplate.
6
+
7
+ In addition, the CLI wrapper includes support for [`cdk8s-local`](../cdk8s-local/README.md) and [`cdk8s-kbld2`](../cdk8s-kbld2/README.md) out of the box, so you can easily run your cdk8s apps locally or run your manifests through `kbld` after synth automatically.
8
+
9
+ ```sh
10
+ # Note: only bun is currently supported as I use `bun` to run shell commands (`kbld`) internally.
11
+ bun i cdk8s-opinionated-cli
12
+ ```
13
+
14
+ ```ts
15
+ import { App } from "cdk8s";
16
+ import { cdk8sOpinionatedCli } from "cdk8s-opinionated-cli";
17
+ import { option, optional, string } from "cmd-ts";
18
+ import { YourConstruct } from "./your-code";
19
+
20
+ await cdk8sOpinionatedCli({
21
+ args: {
22
+ someCustomArg: option({
23
+ type: optional(string),
24
+ long: "some-custom-arg",
25
+ short: "a",
26
+ description: "a custom argument you can use for whatever you want, i usually use one to select a config file.",
27
+ }),
28
+ },
29
+ async synth({ args: { someCustomArg } }) {
30
+ // In this function, construct your app as usual, and return the final app instance.
31
+ const app = new App();
32
+
33
+ new YourConstruct(app, {
34
+ someCustomArg,
35
+ });
36
+
37
+ return app;
38
+ },
39
+ });
40
+ ```
41
+
42
+ You can then synthesise your cdk8s app like so:
43
+
44
+ ```
45
+ bun cli.ts synth --some-custom-arg value
46
+ ```
47
+
48
+ Or deploy it locally, with:
49
+
50
+ ```
51
+ bun cli.ts local --some-custom-arg value
52
+ ```
53
+
54
+ ## cdk8s-kbld2 Integration
55
+
56
+ If a `KbldConfig` construct is detected in your app, `kbld` will be automatically run against your app's generated manifests, and the resulting manifests will be dumped to stdout.
57
+
58
+ ## cdk8s-local Integration
59
+
60
+ You can specify `cdk8s-local` configuration under `subcommands.local`. The configuration is the same as `cdk8s-local`'s, except you can omit the `synth` function from the `local` config. Alternatively, you can specify a `synth` function that is only to be used for local development. See [`cdk8s-local`](../cdk8s-local/README.md) for documentation.
@@ -0,0 +1,86 @@
1
+ import { Config } from "cdk8s-local";
2
+ import { ArgTypes, Output } from "@repo/utils/cmd-ts-types";
3
+ import { Awaitable } from "@repo/utils/awaitable";
4
+ import { CommonContext, CommonStartupContext } from "@repo/utils/cli-contexts";
5
+ import { PickPartial } from "@repo/utils/pick-partial";
6
+ import { App } from "cdk8s";
7
+
8
+ //#region src/config.d.ts
9
+ type FeatureToggle<T extends object> = {
10
+ enabled: false;
11
+ } | ({
12
+ enabled: true;
13
+ } & T);
14
+ interface SynthConfig {
15
+ /**
16
+ * Optional configuration for the generated `cmd-ts` subcommand.
17
+ */
18
+ command?: {
19
+ /**
20
+ * @default "synth"
21
+ */
22
+ name?: string;
23
+ /**
24
+ * @default "Synthesize the CDK8s app into K8s manifests."
25
+ */
26
+ description?: string;
27
+ };
28
+ }
29
+ interface Config$1<Arguments extends ArgTypes, Data, LocalArguments extends ArgTypes> {
30
+ /**
31
+ * `cmd-ts` argument definitions for the CLI. This will be merged with the default arguments provided by `cdk8s-opinionated-cli`,
32
+ * and passed to your hooks and synth function.
33
+ *
34
+ * You can use this to add your own CLI arguments to allow users to configure your CDK8s app generation.
35
+ *
36
+ * This can be overridden for local development (in case you need to pass custom configuration for running locally).
37
+ */
38
+ args?: Arguments;
39
+ /**
40
+ * A function which synthesizes your CDK8s app. Is expected to return an `App` instance.
41
+ */
42
+ synth: (ctx: CommonContext<Output<Arguments>, Data>) => Awaitable<App>;
43
+ /**
44
+ * Optional configuration for the generated `cmd-ts` command.
45
+ */
46
+ command?: {
47
+ /**
48
+ * @default "cdk8s-opinionated-cli"
49
+ */
50
+ name?: string;
51
+ /**
52
+ * @default "CLI for running CDK8s apps with opinionated defaults."
53
+ */
54
+ description?: string;
55
+ };
56
+ /**
57
+ * Configuration for the various subcommands of the CLI. All commands are enabled by default.
58
+ */
59
+ subcommands: {
60
+ /**
61
+ * Optional configuration for the `synth` subcommand.
62
+ */
63
+ synth?: FeatureToggle<SynthConfig>;
64
+ /**
65
+ * Optional configuration for the `local` command. `args` and `synth` are inherited from the main config,
66
+ * but can be extended here.
67
+ */
68
+ local?: FeatureToggle<PickPartial<Config<LocalArguments, Data, Arguments>, "synth">>;
69
+ };
70
+ /**
71
+ * These hooks run at various points and allow you to customize the CLI's behavior.
72
+ *
73
+ * These will be _replaced_ by any hooks provided for individual subcommands.
74
+ */
75
+ hooks?: {
76
+ /**
77
+ * A function that will run immediately on startup, but with access to the parsed CLI arguments.
78
+ *
79
+ * You can return custom data that will be passed to subsequent hooks and the synth function.
80
+ */
81
+ startup?: (ctx: CommonStartupContext<Output<Arguments>>) => Awaitable<Data | void>;
82
+ };
83
+ }
84
+ //#endregion
85
+ export { Config$1 as Config };
86
+ //# sourceMappingURL=config.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.mts","names":[],"sources":["../src/config.ts"],"mappings":";;;;;;;;KAOK,aAAA;EAAoC,OAAA;AAAA;EAAsB,OAAA;AAAA,IAAkB,CAAA;AAAA,UAEhE,WAAA;EAFE;;;EAMlB,OAAA;IANiF;;AAElF;IAQE,IAAA;IAR0B;;;IAY1B,WAAA;EAAA;AAAA;AAAA,UAIe,QAAA,mBAAyB,QAAA,+BAAuC,QAAA;EAAhE;;;;;;;;EAShB,IAAA,GAAO,SAAA;EAIM;;;EAAb,KAAA,GAAQ,GAAA,EAAK,aAAA,CAAc,MAAA,CAAO,SAAA,GAAY,IAAA,MAAU,SAAA,CAAU,GAAA;EAuBzD;;;EAlBT,OAAA;IAuBmC;;;IAnBlC,IAAA;IAiCqC;;;IA7BrC,WAAA;EAAA;EA6BqE;;;EAvBtE,WAAA;IAhCyD;;;IAoCxD,KAAA,GAAQ,aAAA,CAAc,WAAA;IAvBvB;;;;IA4BC,KAAA,GAAQ,aAAA,CAAc,WAAA,CAAY,MAAA,CAAY,cAAA,EAAgB,IAAA,EAAM,SAAA;EAAA;EA5Bb;;;;;EAoCxD,KAAA;IAbC;;;;;IAmBA,OAAA,IAAW,GAAA,EAAK,oBAAA,CAAqB,MAAA,CAAO,SAAA,OAAgB,SAAA,CAAU,IAAA;EAAA;AAAA"}
@@ -0,0 +1,32 @@
1
+ import { Config } from "./config.mjs";
2
+ import * as cmd_ts_dist_cjs_argparser0 from "cmd-ts/dist/cjs/argparser";
3
+ import * as cmd_ts_dist_cjs_helpdoc0 from "cmd-ts/dist/cjs/helpdoc";
4
+ import { Descriptive } from "cmd-ts/dist/cjs/helpdoc";
5
+ import * as cmd_ts_dist_cjs_runner0 from "cmd-ts/dist/cjs/runner";
6
+ import { ArgTypes } from "@repo/utils/cmd-ts-types";
7
+
8
+ //#region src/index.d.ts
9
+ declare function cdk8sOpinionatedCliCommand<Arguments extends ArgTypes, Data, LocalArguments extends ArgTypes>(config: Config<Arguments, Data, LocalArguments>): Promise<Partial<cmd_ts_dist_cjs_argparser0.Register> & {
10
+ parse(context: cmd_ts_dist_cjs_argparser0.ParseContext): Promise<cmd_ts_dist_cjs_argparser0.ParsingResult<{
11
+ command: string;
12
+ args: any;
13
+ }>>;
14
+ } & cmd_ts_dist_cjs_helpdoc0.Named & Partial<Descriptive & cmd_ts_dist_cjs_helpdoc0.Versioned> & cmd_ts_dist_cjs_helpdoc0.PrintHelp & Partial<cmd_ts_dist_cjs_helpdoc0.Versioned> & cmd_ts_dist_cjs_argparser0.Register & cmd_ts_dist_cjs_runner0.Handling<{
15
+ command: string;
16
+ args: any;
17
+ }, {
18
+ command: string;
19
+ value: any;
20
+ }> & {
21
+ run(context: cmd_ts_dist_cjs_argparser0.ParseContext): Promise<cmd_ts_dist_cjs_argparser0.ParsingResult<{
22
+ command: string;
23
+ value: any;
24
+ }>>;
25
+ }>;
26
+ declare function cdk8sOpinionatedCli<Arguments extends ArgTypes, Data, LocalArguments extends ArgTypes>(config: Config<Arguments, Data, LocalArguments>): Promise<{
27
+ command: string;
28
+ value: any;
29
+ }>;
30
+ //#endregion
31
+ export { type Config, cdk8sOpinionatedCli, cdk8sOpinionatedCliCommand };
32
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;;iBAcsB,0BAAA,mBAA6C,QAAA,+BAAuC,QAAA,CAAA,CACzG,MAAA,EAAQ,MAAA,CAAO,SAAA,EAAW,IAAA,EAAM,cAAA,IAAe,OAAA,CAAA,OAAA,CAAjC,0BAAA,CAAiC,QAAA;iBAAA,0BAAA,CAAA,YAAA;;;;;;;;;;;;;;;;iBA+E1B,mBAAA,mBAAsC,QAAA,+BAAuC,QAAA,CAAA,CAClG,MAAA,EAAQ,MAAA,CAAO,SAAA,EAAW,IAAA,EAAM,cAAA,IAAe,OAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,86 @@
1
+ import { logger } from "@repo/utils/logger";
2
+ import { $ } from "bun";
3
+ import { KbldConfig } from "cdk8s-kbld2";
4
+ import { binary, boolean, command, flag, run, subcommands } from "cmd-ts";
5
+ import { ConstructOrder } from "constructs";
6
+ import { rm } from "fs/promises";
7
+ import path from "path";
8
+
9
+ //#region src/index.ts
10
+ async function cdk8sOpinionatedCliCommand(config) {
11
+ const cmds = {};
12
+ if (config.subcommands.local?.enabled !== false) {
13
+ const { cdk8sLocalCommand } = await import("cdk8s-local");
14
+ cmds["local"] = cdk8sLocalCommand({
15
+ synth: config.synth,
16
+ ...config.subcommands.local,
17
+ args: {
18
+ ...config.args,
19
+ ...config.subcommands.local?.args
20
+ },
21
+ hooks: {
22
+ ...config.hooks,
23
+ ...config.subcommands.local?.hooks
24
+ }
25
+ });
26
+ }
27
+ if (config.subcommands.synth?.enabled !== false) cmds["synth"] = command({
28
+ name: config.subcommands.synth?.command?.name ?? "synth",
29
+ description: config.subcommands.synth?.command?.description ?? "Synthesize the CDK8s app into K8s manifests.",
30
+ args: {
31
+ noBuild: flag({
32
+ type: boolean,
33
+ long: "no-build",
34
+ description: "only synthesise the manifests, do not build resulting manifests with kbld"
35
+ }),
36
+ ...config.args
37
+ },
38
+ async handler(args) {
39
+ const startupCtx = {
40
+ args,
41
+ command: "synth"
42
+ };
43
+ const ctx = {
44
+ ...startupCtx,
45
+ data: await config.hooks?.startup?.(startupCtx) ?? {}
46
+ };
47
+ logger.info("Running synth (generating manifests)...");
48
+ const app = await config.synth(ctx);
49
+ await rm(app.outdir, {
50
+ recursive: true,
51
+ force: true
52
+ });
53
+ app.synth();
54
+ if (args.noBuild) {
55
+ logger.info("Synth complete, skipping build, find manifests in " + path.relative(process.cwd(), app.outdir));
56
+ return;
57
+ }
58
+ if (app.node.findAll(ConstructOrder.POSTORDER).find((c) => c instanceof KbldConfig)) {
59
+ logger.info("Detected kbld config construct, running kbld...");
60
+ try {
61
+ const buffer = await $`kbld -f ${app.outdir}`.arrayBuffer();
62
+ process.stdout.write(Buffer.from(buffer));
63
+ } catch (e) {
64
+ if (e instanceof $.ShellError) {
65
+ logger.error(e.stderr.toString("utf-8"));
66
+ logger.fatal("Failed to build images from manifests.");
67
+ return 1;
68
+ }
69
+ throw e;
70
+ }
71
+ }
72
+ }
73
+ });
74
+ return subcommands({
75
+ name: config.command?.name ?? "cdk8s-opinionated-cli",
76
+ description: config.command?.description ?? "CLI for running CDK8s apps with opinionated defaults.",
77
+ cmds
78
+ });
79
+ }
80
+ async function cdk8sOpinionatedCli(config) {
81
+ return await run(binary(await cdk8sOpinionatedCliCommand(config)), process.argv);
82
+ }
83
+
84
+ //#endregion
85
+ export { cdk8sOpinionatedCli, cdk8sOpinionatedCliCommand };
86
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { ArgTypes } from \"@repo/utils/cmd-ts-types\";\nimport { logger } from \"@repo/utils/logger\";\nimport { $ } from \"bun\";\nimport { KbldConfig } from \"cdk8s-kbld2\";\nimport { binary, boolean, command, flag, run, type Runner, subcommands } from \"cmd-ts\";\nimport type { ArgParser } from \"cmd-ts/dist/cjs/argparser\";\nimport type { Aliased, Descriptive } from \"cmd-ts/dist/cjs/helpdoc\";\nimport { ConstructOrder } from \"constructs\";\nimport { rm } from \"fs/promises\";\nimport path from \"path\";\nimport type { Config } from \"./config\";\n\nexport type { Config } from \"./config\";\n\nexport async function cdk8sOpinionatedCliCommand<Arguments extends ArgTypes, Data, LocalArguments extends ArgTypes>(\n\tconfig: Config<Arguments, Data, LocalArguments>,\n) {\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tconst cmds: Record<string, ArgParser<any> & Runner<any, any> & Partial<Descriptive & Aliased>> = {};\n\n\tif (config.subcommands.local?.enabled !== false) {\n\t\tconst { cdk8sLocalCommand } = await import(\"cdk8s-local\");\n\t\tconst cmd = cdk8sLocalCommand<Arguments & LocalArguments, Data>({\n\t\t\tsynth: config.synth,\n\t\t\t...config.subcommands.local,\n\t\t\targs: {\n\t\t\t\t// Spreading `undefined` is safe and results in no additional arguments, which is the expected behaviour.\n\t\t\t\t...config.args!,\n\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain\n\t\t\t\t...config.subcommands.local?.args!,\n\t\t\t},\n\t\t\thooks: {\n\t\t\t\t...config.hooks,\n\t\t\t\t...config.subcommands.local?.hooks,\n\t\t\t},\n\t\t});\n\t\tcmds[\"local\"] = cmd;\n\t}\n\n\tif (config.subcommands.synth?.enabled !== false) {\n\t\tcmds[\"synth\"] = command({\n\t\t\tname: config.subcommands.synth?.command?.name ?? \"synth\",\n\t\t\tdescription: config.subcommands.synth?.command?.description ?? \"Synthesize the CDK8s app into K8s manifests.\",\n\t\t\targs: {\n\t\t\t\tnoBuild: flag({\n\t\t\t\t\ttype: boolean,\n\t\t\t\t\tlong: \"no-build\",\n\t\t\t\t\tdescription: \"only synthesise the manifests, do not build resulting manifests with kbld\",\n\t\t\t\t}),\n\t\t\t\t...config.args!,\n\t\t\t},\n\t\t\tasync handler(args) {\n\t\t\t\tconst startupCtx = { args, command: \"synth\" as const };\n\t\t\t\tconst ctx = {\n\t\t\t\t\t...startupCtx,\n\t\t\t\t\tdata: (await config.hooks?.startup?.(startupCtx)) ?? ({} as Data),\n\t\t\t\t};\n\n\t\t\t\tlogger.info(\"Running synth (generating manifests)...\");\n\n\t\t\t\tconst app = await config.synth(ctx);\n\t\t\t\tawait rm(app.outdir, { recursive: true, force: true });\n\t\t\t\tapp.synth();\n\n\t\t\t\tif (args.noBuild) {\n\t\t\t\t\tlogger.info(\"Synth complete, skipping build, find manifests in \" + path.relative(process.cwd(), app.outdir));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (app.node.findAll(ConstructOrder.POSTORDER).find((c) => c instanceof KbldConfig)) {\n\t\t\t\t\tlogger.info(\"Detected kbld config construct, running kbld...\");\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst buffer = await $`kbld -f ${app.outdir}`.arrayBuffer();\n\t\t\t\t\t\tprocess.stdout.write(Buffer.from(buffer));\n\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\tif (e instanceof $.ShellError) {\n\t\t\t\t\t\t\tlogger.error(e.stderr.toString(\"utf-8\"));\n\t\t\t\t\t\t\tlogger.fatal(\"Failed to build images from manifests.\");\n\t\t\t\t\t\t\treturn 1;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthrow e;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t}\n\n\treturn subcommands({\n\t\tname: config.command?.name ?? \"cdk8s-opinionated-cli\",\n\t\tdescription: config.command?.description ?? \"CLI for running CDK8s apps with opinionated defaults.\",\n\t\tcmds,\n\t});\n}\n\nexport async function cdk8sOpinionatedCli<Arguments extends ArgTypes, Data, LocalArguments extends ArgTypes>(\n\tconfig: Config<Arguments, Data, LocalArguments>,\n) {\n\tconst cmd = await cdk8sOpinionatedCliCommand(config);\n\treturn await run(binary(cmd), process.argv);\n}\n"],"mappings":";;;;;;;;;AAcA,eAAsB,2BACrB,QACC;CAED,MAAM,OAA2F,EAAE;AAEnG,KAAI,OAAO,YAAY,OAAO,YAAY,OAAO;EAChD,MAAM,EAAE,sBAAsB,MAAM,OAAO;AAe3C,OAAK,WAdO,kBAAoD;GAC/D,OAAO,OAAO;GACd,GAAG,OAAO,YAAY;GACtB,MAAM;IAEL,GAAG,OAAO;IAEV,GAAG,OAAO,YAAY,OAAO;IAC7B;GACD,OAAO;IACN,GAAG,OAAO;IACV,GAAG,OAAO,YAAY,OAAO;IAC7B;GACD,CAAC;;AAIH,KAAI,OAAO,YAAY,OAAO,YAAY,MACzC,MAAK,WAAW,QAAQ;EACvB,MAAM,OAAO,YAAY,OAAO,SAAS,QAAQ;EACjD,aAAa,OAAO,YAAY,OAAO,SAAS,eAAe;EAC/D,MAAM;GACL,SAAS,KAAK;IACb,MAAM;IACN,MAAM;IACN,aAAa;IACb,CAAC;GACF,GAAG,OAAO;GACV;EACD,MAAM,QAAQ,MAAM;GACnB,MAAM,aAAa;IAAE;IAAM,SAAS;IAAkB;GACtD,MAAM,MAAM;IACX,GAAG;IACH,MAAO,MAAM,OAAO,OAAO,UAAU,WAAW,IAAM,EAAE;IACxD;AAED,UAAO,KAAK,0CAA0C;GAEtD,MAAM,MAAM,MAAM,OAAO,MAAM,IAAI;AACnC,SAAM,GAAG,IAAI,QAAQ;IAAE,WAAW;IAAM,OAAO;IAAM,CAAC;AACtD,OAAI,OAAO;AAEX,OAAI,KAAK,SAAS;AACjB,WAAO,KAAK,uDAAuD,KAAK,SAAS,QAAQ,KAAK,EAAE,IAAI,OAAO,CAAC;AAC5G;;AAGD,OAAI,IAAI,KAAK,QAAQ,eAAe,UAAU,CAAC,MAAM,MAAM,aAAa,WAAW,EAAE;AACpF,WAAO,KAAK,kDAAkD;AAC9D,QAAI;KACH,MAAM,SAAS,MAAM,CAAC,WAAW,IAAI,SAAS,aAAa;AAC3D,aAAQ,OAAO,MAAM,OAAO,KAAK,OAAO,CAAC;aACjC,GAAG;AACX,SAAI,aAAa,EAAE,YAAY;AAC9B,aAAO,MAAM,EAAE,OAAO,SAAS,QAAQ,CAAC;AACxC,aAAO,MAAM,yCAAyC;AACtD,aAAO;;AAER,WAAM;;;;EAIT,CAAC;AAGH,QAAO,YAAY;EAClB,MAAM,OAAO,SAAS,QAAQ;EAC9B,aAAa,OAAO,SAAS,eAAe;EAC5C;EACA,CAAC;;AAGH,eAAsB,oBACrB,QACC;AAED,QAAO,MAAM,IAAI,OADL,MAAM,2BAA2B,OAAO,CACxB,EAAE,QAAQ,KAAK"}
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "cdk8s-opinionated-cli",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "scripts": {
7
+ "lint": "eslint . --max-warnings 0",
8
+ "typecheck": "tsc --noEmit",
9
+ "dist": "bun --bun tsdown",
10
+ "publish-pkg": "bun publish --tolerate-republish"
11
+ },
12
+ "dependencies": {
13
+ "cdk8s": "^2.70.28",
14
+ "cdk8s-kbld2": "0.1.0",
15
+ "cdk8s-local": "0.1.0",
16
+ "cmd-ts": "^0.14.3",
17
+ "constructs": "^10.4.3",
18
+ "@repo/utils": "0.0.0"
19
+ },
20
+ "devDependencies": {
21
+ "@repo/eslint-config": "0.0.0",
22
+ "@repo/typescript-config": "0.0.0",
23
+ "@types/bun": "^1.3.8",
24
+ "eslint": "^9.34.0",
25
+ "tsdown": "^0.20.2",
26
+ "typescript": "^5.9.3"
27
+ },
28
+ "exports": {
29
+ ".": {
30
+ "bun": "./src/index.ts",
31
+ "default": "./dist/index.mjs"
32
+ },
33
+ "./package.json": "./package.json"
34
+ }
35
+ }
package/src/config.ts ADDED
@@ -0,0 +1,83 @@
1
+ import type { Awaitable } from "@repo/utils/awaitable";
2
+ import type { CommonContext, CommonStartupContext } from "@repo/utils/cli-contexts";
3
+ import type { ArgTypes, Output } from "@repo/utils/cmd-ts-types";
4
+ import type { PickPartial } from "@repo/utils/pick-partial";
5
+ import type { App } from "cdk8s";
6
+ import { Config as LocalConfig } from "cdk8s-local";
7
+
8
+ type FeatureToggle<T extends object> = { enabled: false } | ({ enabled: true } & T);
9
+
10
+ export interface SynthConfig {
11
+ /**
12
+ * Optional configuration for the generated `cmd-ts` subcommand.
13
+ */
14
+ command?: {
15
+ /**
16
+ * @default "synth"
17
+ */
18
+ name?: string;
19
+ /**
20
+ * @default "Synthesize the CDK8s app into K8s manifests."
21
+ */
22
+ description?: string;
23
+ };
24
+ }
25
+
26
+ export interface Config<Arguments extends ArgTypes, Data, LocalArguments extends ArgTypes> {
27
+ /**
28
+ * `cmd-ts` argument definitions for the CLI. This will be merged with the default arguments provided by `cdk8s-opinionated-cli`,
29
+ * and passed to your hooks and synth function.
30
+ *
31
+ * You can use this to add your own CLI arguments to allow users to configure your CDK8s app generation.
32
+ *
33
+ * This can be overridden for local development (in case you need to pass custom configuration for running locally).
34
+ */
35
+ args?: Arguments;
36
+ /**
37
+ * A function which synthesizes your CDK8s app. Is expected to return an `App` instance.
38
+ */
39
+ synth: (ctx: CommonContext<Output<Arguments>, Data>) => Awaitable<App>;
40
+
41
+ /**
42
+ * Optional configuration for the generated `cmd-ts` command.
43
+ */
44
+ command?: {
45
+ /**
46
+ * @default "cdk8s-opinionated-cli"
47
+ */
48
+ name?: string;
49
+ /**
50
+ * @default "CLI for running CDK8s apps with opinionated defaults."
51
+ */
52
+ description?: string;
53
+ };
54
+
55
+ /**
56
+ * Configuration for the various subcommands of the CLI. All commands are enabled by default.
57
+ */
58
+ subcommands: {
59
+ /**
60
+ * Optional configuration for the `synth` subcommand.
61
+ */
62
+ synth?: FeatureToggle<SynthConfig>;
63
+ /**
64
+ * Optional configuration for the `local` command. `args` and `synth` are inherited from the main config,
65
+ * but can be extended here.
66
+ */
67
+ local?: FeatureToggle<PickPartial<LocalConfig<LocalArguments, Data, Arguments>, "synth">>;
68
+ };
69
+
70
+ /**
71
+ * These hooks run at various points and allow you to customize the CLI's behavior.
72
+ *
73
+ * These will be _replaced_ by any hooks provided for individual subcommands.
74
+ */
75
+ hooks?: {
76
+ /**
77
+ * A function that will run immediately on startup, but with access to the parsed CLI arguments.
78
+ *
79
+ * You can return custom data that will be passed to subsequent hooks and the synth function.
80
+ */
81
+ startup?: (ctx: CommonStartupContext<Output<Arguments>>) => Awaitable<Data | void>;
82
+ };
83
+ }
package/src/index.ts ADDED
@@ -0,0 +1,100 @@
1
+ import type { ArgTypes } from "@repo/utils/cmd-ts-types";
2
+ import { logger } from "@repo/utils/logger";
3
+ import { $ } from "bun";
4
+ import { KbldConfig } from "cdk8s-kbld2";
5
+ import { binary, boolean, command, flag, run, type Runner, subcommands } from "cmd-ts";
6
+ import type { ArgParser } from "cmd-ts/dist/cjs/argparser";
7
+ import type { Aliased, Descriptive } from "cmd-ts/dist/cjs/helpdoc";
8
+ import { ConstructOrder } from "constructs";
9
+ import { rm } from "fs/promises";
10
+ import path from "path";
11
+ import type { Config } from "./config";
12
+
13
+ export type { Config } from "./config";
14
+
15
+ export async function cdk8sOpinionatedCliCommand<Arguments extends ArgTypes, Data, LocalArguments extends ArgTypes>(
16
+ config: Config<Arguments, Data, LocalArguments>,
17
+ ) {
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ const cmds: Record<string, ArgParser<any> & Runner<any, any> & Partial<Descriptive & Aliased>> = {};
20
+
21
+ if (config.subcommands.local?.enabled !== false) {
22
+ const { cdk8sLocalCommand } = await import("cdk8s-local");
23
+ const cmd = cdk8sLocalCommand<Arguments & LocalArguments, Data>({
24
+ synth: config.synth,
25
+ ...config.subcommands.local,
26
+ args: {
27
+ // Spreading `undefined` is safe and results in no additional arguments, which is the expected behaviour.
28
+ ...config.args!,
29
+ // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
30
+ ...config.subcommands.local?.args!,
31
+ },
32
+ hooks: {
33
+ ...config.hooks,
34
+ ...config.subcommands.local?.hooks,
35
+ },
36
+ });
37
+ cmds["local"] = cmd;
38
+ }
39
+
40
+ if (config.subcommands.synth?.enabled !== false) {
41
+ cmds["synth"] = command({
42
+ name: config.subcommands.synth?.command?.name ?? "synth",
43
+ description: config.subcommands.synth?.command?.description ?? "Synthesize the CDK8s app into K8s manifests.",
44
+ args: {
45
+ noBuild: flag({
46
+ type: boolean,
47
+ long: "no-build",
48
+ description: "only synthesise the manifests, do not build resulting manifests with kbld",
49
+ }),
50
+ ...config.args!,
51
+ },
52
+ async handler(args) {
53
+ const startupCtx = { args, command: "synth" as const };
54
+ const ctx = {
55
+ ...startupCtx,
56
+ data: (await config.hooks?.startup?.(startupCtx)) ?? ({} as Data),
57
+ };
58
+
59
+ logger.info("Running synth (generating manifests)...");
60
+
61
+ const app = await config.synth(ctx);
62
+ await rm(app.outdir, { recursive: true, force: true });
63
+ app.synth();
64
+
65
+ if (args.noBuild) {
66
+ logger.info("Synth complete, skipping build, find manifests in " + path.relative(process.cwd(), app.outdir));
67
+ return;
68
+ }
69
+
70
+ if (app.node.findAll(ConstructOrder.POSTORDER).find((c) => c instanceof KbldConfig)) {
71
+ logger.info("Detected kbld config construct, running kbld...");
72
+ try {
73
+ const buffer = await $`kbld -f ${app.outdir}`.arrayBuffer();
74
+ process.stdout.write(Buffer.from(buffer));
75
+ } catch (e) {
76
+ if (e instanceof $.ShellError) {
77
+ logger.error(e.stderr.toString("utf-8"));
78
+ logger.fatal("Failed to build images from manifests.");
79
+ return 1;
80
+ }
81
+ throw e;
82
+ }
83
+ }
84
+ },
85
+ });
86
+ }
87
+
88
+ return subcommands({
89
+ name: config.command?.name ?? "cdk8s-opinionated-cli",
90
+ description: config.command?.description ?? "CLI for running CDK8s apps with opinionated defaults.",
91
+ cmds,
92
+ });
93
+ }
94
+
95
+ export async function cdk8sOpinionatedCli<Arguments extends ArgTypes, Data, LocalArguments extends ArgTypes>(
96
+ config: Config<Arguments, Data, LocalArguments>,
97
+ ) {
98
+ const cmd = await cdk8sOpinionatedCliCommand(config);
99
+ return await run(binary(cmd), process.argv);
100
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "@repo/typescript-config/tsconfig.base.json",
3
+ "include": ["src", "tsdown.config.ts"]
4
+ }