Skip to content

Commit ec2ca36

Browse files
committed
feat: dynamic create-commandkit
1 parent 0bffa78 commit ec2ca36

File tree

30 files changed

+958
-2608
lines changed

30 files changed

+958
-2608
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
node_modules
22
dist
33
.DS_Store
4-
.vscode
54
.turbo

.vscode/settings.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"files.readonlyInclude": {
3+
"**/routeTree.gen.ts": true
4+
},
5+
"files.watcherExclude": {
6+
"**/routeTree.gen.ts": true
7+
},
8+
"search.exclude": {
9+
"**/routeTree.gen.ts": true
10+
}
11+
}

examples/without-cli/pnpm-lock.yaml

Lines changed: 0 additions & 1726 deletions
This file was deleted.

packages/create-commandkit/README.md

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,85 @@ create-commandkit is a CLI utility to quickly instantiate a Discord bot with Com
1414

1515
- Interactive, beautiful command-line interface 🖥️
1616
- Supports CommonJS and ES Modules 📦
17+
- Dynamic template system with examples from GitHub 🚀
18+
- Support for all major package managers (npm, pnpm, yarn, bun, deno) 📦
19+
- TypeScript and JavaScript support 🔧
1720

1821
## Documentation
1922

2023
You can find the full documentation [here](https://commandkit.dev).
2124

2225
## Usage
2326

24-
Run the following command in your terminal:
27+
### Basic Usage
2528

2629
```sh
2730
npx create-commandkit@latest
2831
```
2932

33+
### With Project Name
34+
35+
```sh
36+
npx create-commandkit@latest my-bot
37+
```
38+
39+
### Using Examples
40+
41+
```sh
42+
# Use a curated example
43+
npx create-commandkit@latest --example with-database
44+
45+
# Use a custom GitHub repository
46+
npx create-commandkit@latest --example "https://github.com/user/repo"
47+
48+
# Use a specific path within a repository
49+
npx create-commandkit@latest --example "https://github.com/user/repo" --example-path "examples/bot"
50+
```
51+
52+
### CLI Options
53+
54+
- `-h, --help` - Show all available options
55+
- `-v, --version` - Output the version number
56+
- `-e, --example <name-or-url>` - An example to bootstrap the app with
57+
- `--example-path <path>` - Specify the path to the example separately
58+
- `--use-npm` - Use npm as package manager
59+
- `--use-pnpm` - Use pnpm as package manager
60+
- `--use-yarn` - Use yarn as package manager
61+
- `--use-bun` - Use bun as package manager
62+
- `--use-deno` - Use deno as package manager
63+
- `--skip-install` - Skip installing packages
64+
- `--no-git` - Skip git initialization
65+
- `--yes` - Use defaults for all options
66+
- `--list-examples` - List all available examples from the official repository
67+
68+
### Available Examples
69+
70+
<!-- BEGIN_AVAILABLE_EXAMPLES -->
71+
- `basic-js` - [examples/basic-js](https://github.com/underctrl-io/commandkit/tree/main/examples/basic-js)
72+
- `basic-ts` - [examples/basic-ts](https://github.com/underctrl-io/commandkit/tree/main/examples/basic-ts)
73+
- `deno-ts` - [examples/deno-ts](https://github.com/underctrl-io/commandkit/tree/main/examples/deno-ts)
74+
- `without-cli` - [examples/without-cli](https://github.com/underctrl-io/commandkit/tree/main/examples/without-cli)
75+
<!-- END_AVAILABLE_EXAMPLES -->
76+
77+
### Examples
78+
79+
```sh
80+
# Create a bot with database example, skip installation
81+
npx create-commandkit@latest --example with-database --skip-install
82+
83+
# Create a bot with all defaults (no prompts)
84+
npx create-commandkit@latest --yes
85+
86+
# Create a bot from custom repository
87+
npx create-commandkit@latest --example "https://github.com/username/my-commandkit-template"
88+
89+
# Create a bot with pnpm
90+
npx create-commandkit@latest --use-pnpm
91+
92+
# List all available examples
93+
npx create-commandkit@latest --list-examples
94+
```
95+
3096
## Support and Suggestions
3197

3298
Submit any queries or suggestions in our [Discord community](https://ctrl.lol/discord).

packages/create-commandkit/package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,25 @@
3030
"scripts": {
3131
"check-types": "tsc --noEmit",
3232
"dev": "tsc --watch",
33-
"build": "tsc"
33+
"build": "tsc && tsx scripts/sync-available-examples.ts"
3434
},
3535
"dependencies": {
3636
"@clack/prompts": "^0.11.0",
37+
"commander": "^14.0.1",
3738
"fs-extra": "^11.1.1",
3839
"gradient-string": "^3.0.0",
3940
"ora": "^8.0.1",
40-
"picocolors": "^1.1.1"
41+
"picocolors": "^1.1.1",
42+
"tiged": "^2.12.7",
43+
"validate-npm-package-name": "^6.0.2"
4144
},
4245
"devDependencies": {
4346
"@types/fs-extra": "^11.0.4",
4447
"@types/gradient-string": "^1.1.5",
4548
"@types/node": "^22.0.0",
49+
"@types/validate-npm-package-name": "^4.0.2",
4650
"tsconfig": "workspace:*",
51+
"tsx": "^4.20.6",
4752
"typescript": "catalog:build"
4853
}
49-
}
54+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import path from 'node:path';
2+
import fs from 'node:fs';
3+
4+
const BEGIN_MARKER = '<!-- BEGIN_AVAILABLE_EXAMPLES -->';
5+
const END_MARKER = '<!-- END_AVAILABLE_EXAMPLES -->';
6+
7+
const README_PATH = path.join(import.meta.dirname, '..', 'README.md');
8+
const README_CONTENT = fs.readFileSync(README_PATH, 'utf8');
9+
10+
function insertBetween(content: string) {
11+
const availableExamples = fs.readdirSync(
12+
path.join(import.meta.dirname, '..', '..', '..', 'examples'),
13+
);
14+
15+
const examples = availableExamples
16+
.map(
17+
(example) =>
18+
`- \`${example}\` - [examples/${example}](https://github.com/underctrl-io/commandkit/tree/main/examples/${example})`,
19+
)
20+
.join('\n');
21+
22+
const between = `${BEGIN_MARKER}\n${examples}\n${END_MARKER}`;
23+
const newContent = [
24+
content.split(BEGIN_MARKER)[0],
25+
between,
26+
content.split(END_MARKER)[1],
27+
].join('');
28+
29+
return newContent;
30+
}
31+
32+
const newContent = insertBetween(README_CONTENT);
33+
34+
fs.writeFileSync(README_PATH, newContent);
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Command } from 'commander';
2+
import type { CLIOptions } from './types.js';
3+
4+
export function parseCLI(): CLIOptions {
5+
const program = new Command();
6+
7+
program
8+
.name('create-commandkit')
9+
.description('Effortlessly create a CommandKit project')
10+
.version(process.env.npm_package_version || '1.0.0')
11+
.argument('[project-directory]', 'Project directory name')
12+
.option('-h, --help', 'Show all available options')
13+
.option('-v, --version', 'Output the version number')
14+
.option(
15+
'-e, --example <name-or-url>',
16+
'An example to bootstrap the app with',
17+
)
18+
.option(
19+
'--example-path <path>',
20+
'Specify the path to the example separately',
21+
)
22+
.option('--use-npm', 'Explicitly tell the CLI to bootstrap using npm')
23+
.option('--use-pnpm', 'Explicitly tell the CLI to bootstrap using pnpm')
24+
.option('--use-yarn', 'Explicitly tell the CLI to bootstrap using yarn')
25+
.option('--use-bun', 'Explicitly tell the CLI to bootstrap using bun')
26+
.option('--use-deno', 'Explicitly tell the CLI to bootstrap using deno')
27+
.option(
28+
'--skip-install',
29+
'Explicitly tell the CLI to skip installing packages',
30+
)
31+
.option('--no-git', 'Explicitly tell the CLI to disable git initialization')
32+
.option('--yes', 'Use previous preferences or defaults for all options')
33+
.option(
34+
'--list-examples',
35+
'List all available examples from the official repository',
36+
);
37+
38+
program.parse();
39+
40+
const options = program.opts();
41+
const args = program.args;
42+
43+
return {
44+
help: options.help,
45+
version: options.version,
46+
example: options.example,
47+
examplePath: options.examplePath,
48+
useNpm: options.useNpm,
49+
usePnpm: options.usePnpm,
50+
useYarn: options.useYarn,
51+
useBun: options.useBun,
52+
useDeno: options.useDeno,
53+
skipInstall: options.skipInstall,
54+
noGit: options.noGit,
55+
yes: options.yes,
56+
listExamples: options.listExamples,
57+
projectDirectory: args[0],
58+
};
59+
}

packages/create-commandkit/src/functions/copyTemplates.ts

Lines changed: 0 additions & 57 deletions
This file was deleted.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import fs from 'fs-extra';
2+
import path from 'node:path';
3+
// @ts-ignore
4+
import { tiged } from 'tiged';
5+
import { validateExampleName } from './validate.js';
6+
7+
export interface FetchExampleOptions {
8+
example: string;
9+
examplePath?: string;
10+
targetDir: string;
11+
}
12+
13+
export async function fetchExample({
14+
example,
15+
examplePath,
16+
targetDir,
17+
}: FetchExampleOptions): Promise<void> {
18+
const validation = validateExampleName(example);
19+
if (!validation.valid) {
20+
throw new Error(validation.error);
21+
}
22+
23+
let sourceUrl: string;
24+
25+
// Check if it's a GitHub URL
26+
if (example.startsWith('http://') || example.startsWith('https://')) {
27+
sourceUrl = example;
28+
} else {
29+
// Construct URL for curated examples
30+
sourceUrl = `underctrl-io/commandkit/examples/${example}`;
31+
}
32+
33+
// Create temporary directory for download
34+
const tempDir = path.join(targetDir, '.temp-example');
35+
36+
try {
37+
// Download the example
38+
const emitter = tiged(sourceUrl, {
39+
mode: 'tar',
40+
disableCache: true,
41+
});
42+
43+
await emitter.clone(tempDir);
44+
45+
// If examplePath is specified, navigate to that subdirectory
46+
const sourceDir = examplePath ? path.join(tempDir, examplePath) : tempDir;
47+
48+
if (examplePath && !fs.existsSync(sourceDir)) {
49+
throw new Error(
50+
`Example path '${examplePath}' not found in the repository`,
51+
);
52+
}
53+
54+
// Copy contents to target directory
55+
const contents = fs.readdirSync(sourceDir);
56+
57+
for (const item of contents) {
58+
const srcPath = path.join(sourceDir, item);
59+
const destPath = path.join(targetDir, item);
60+
61+
if (fs.statSync(srcPath).isDirectory()) {
62+
await fs.copy(srcPath, destPath);
63+
} else {
64+
await fs.copy(srcPath, destPath);
65+
}
66+
}
67+
68+
// Clean up temporary directory
69+
await fs.remove(tempDir);
70+
} catch (error) {
71+
// Clean up on error
72+
if (fs.existsSync(tempDir)) {
73+
await fs.remove(tempDir);
74+
}
75+
76+
if (error instanceof Error) {
77+
if (
78+
error.message.includes('not found') ||
79+
error.message.includes('404')
80+
) {
81+
throw new Error(
82+
`Example '${example}' not found. Available examples: basic-ts, basic-js, with-database, with-i18n`,
83+
);
84+
}
85+
throw new Error(`Failed to fetch example: ${error.message}`);
86+
}
87+
88+
throw new Error('Failed to fetch example due to an unknown error');
89+
}
90+
}

0 commit comments

Comments
 (0)