├── .eslintignore ├── .github ├── hooks │ ├── commit-msg │ └── pre-commit ├── CODEOWNERS ├── renovate.json └── workflows │ ├── labelsync.yml │ ├── codeql-analysis.yml │ ├── auto-deprecate.yml │ ├── publish.yml │ ├── deprecate-on-merge.yml │ ├── continuous-integration.yml │ └── continuous-delivery.yml ├── .npm-deprecaterc.yml ├── .prettierignore ├── tsconfig.eslint.json ├── .eslintrc ├── .vscode ├── extensions.json └── settings.json ├── sonar-project.properties ├── src ├── tsconfig.json ├── functions │ ├── FileExists.ts │ ├── generateCommandFlow.ts │ ├── CreateFileFromTemplate.ts │ ├── CommandExists.ts │ └── CreateComponentLoader.ts ├── lib │ ├── aliases.ts │ └── types.ts ├── constants.ts ├── commands │ ├── generate-loader.ts │ ├── init.ts │ ├── generate.ts │ └── new.ts ├── cli.ts └── prompts │ ├── PromptInit.ts │ └── PromptNew.ts ├── templates ├── components │ ├── listener.ts.sapphire │ ├── listener.js.sapphire │ ├── argument.ts.sapphire │ ├── messagecommand.ts.sapphire │ ├── argument.js.sapphire │ ├── messagecommand.js.sapphire │ ├── route.ts.sapphire │ ├── precondition.js.sapphire │ ├── slashcommand.ts.sapphire │ ├── precondition.ts.sapphire │ ├── contextmenucommand.ts.sapphire │ ├── modalinteractionhandler.ts.sapphire │ ├── buttoninteractionhandler.ts.sapphire │ ├── selectmenuinteractionhandler.ts.sapphire │ ├── route.js.sapphire │ ├── slashcommand.js.sapphire │ ├── contextmenucommand.js.sapphire │ ├── modalinteractionhandler.js.sapphire │ ├── selectmenuinteractionhandler.js.sapphire │ ├── buttoninteractionhandler.js.sapphire │ ├── autocompleteinteractionhandler.ts.sapphire │ └── autocompleteinteractionhandler.js.sapphire ├── .sapphirerc.yml.sapphire ├── .sapphirerc.json.sapphire └── schemas │ └── .sapphirerc.scheme.json ├── .cliff-jumperrc.yml ├── .yarnrc.yml ├── .gitignore ├── tsconfig.base.json ├── .editorconfig ├── LICENSE.md ├── README.md ├── cliff.toml ├── package.json ├── .yarn └── plugins │ └── @yarnpkg │ └── plugin-git-hooks.cjs └── CHANGELOG.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /.github/hooks/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | yarn commitlint --edit $1 -------------------------------------------------------------------------------- /.github/hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | yarn lint-staged 4 | -------------------------------------------------------------------------------- /.npm-deprecaterc.yml: -------------------------------------------------------------------------------- 1 | name: '*next*' 2 | package: 3 | - '@sapphire/cli' 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | .yarn/ 4 | 5 | CHANGELOG.md 6 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | /src/ @enxg @favna @vladfrangu @kyranet 2 | /templates/ @enxg 3 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["src", "scripts", "templates"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sapphire", 3 | "parserOptions": { 4 | "extraFileExtensions": [".sapphire"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["bierner.github-markdown-preview", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=sapphiredev_cli 2 | sonar.organization=sapphiredev 3 | sonar.pullrequest.github.summary_comment=false 4 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>sapphiredev/.github:sapphire-renovate"] 4 | } 5 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "./", 5 | "outDir": "../dist", 6 | "composite": true, 7 | "preserveConstEnums": true 8 | }, 9 | "include": ["."] 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/labelsync.yml: -------------------------------------------------------------------------------- 1 | name: Automatic Label Sync 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | label_sync: 10 | uses: sapphiredev/.github/.github/workflows/reusable-labelsync.yml@main 11 | -------------------------------------------------------------------------------- /src/functions/FileExists.ts: -------------------------------------------------------------------------------- 1 | import { Result } from '@sapphire/result'; 2 | import { access } from 'node:fs/promises'; 3 | 4 | export async function fileExists(filePath: string): Promise { 5 | const result = await Result.fromAsync(() => access(filePath)); 6 | 7 | return result.isOk(); 8 | } 9 | -------------------------------------------------------------------------------- /templates/components/listener.ts.sapphire: -------------------------------------------------------------------------------- 1 | { "category": "listeners" } 2 | --- 3 | import { ApplyOptions } from '@sapphire/decorators'; 4 | import { Listener } from '@sapphire/framework'; 5 | 6 | @ApplyOptions({}) 7 | export class UserEvent extends Listener { 8 | public override run() {} 9 | } 10 | -------------------------------------------------------------------------------- /.cliff-jumperrc.yml: -------------------------------------------------------------------------------- 1 | name: cli 2 | packagePath: . 3 | org: sapphire 4 | monoRepo: false 5 | commitMessageTemplate: 'chore(release): release {{new-version}}' 6 | tagTemplate: v{{new-version}} 7 | identifierBase: false 8 | pushTag: true 9 | githubRelease: true 10 | githubReleaseLatest: true 11 | gitRepo: sapphiredev/cli 12 | gitHostVariant: github 13 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Code scanning 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | schedule: 11 | - cron: '30 1 * * 0' 12 | 13 | jobs: 14 | codeql: 15 | name: Analysis 16 | uses: sapphiredev/.github/.github/workflows/reusable-codeql.yml@main 17 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: true 4 | 5 | gitHooksPath: .github/hooks 6 | 7 | nodeLinker: node-modules 8 | 9 | plugins: 10 | - path: .yarn/plugins/@yarnpkg/plugin-git-hooks.cjs 11 | spec: 'https://raw.githubusercontent.com/trufflehq/yarn-plugin-git-hooks/main/bundles/%40yarnpkg/plugin-git-hooks.js' 12 | 13 | yarnPath: .yarn/releases/yarn-4.12.0.cjs 14 | -------------------------------------------------------------------------------- /.github/workflows/auto-deprecate.yml: -------------------------------------------------------------------------------- 1 | name: NPM Auto Deprecate 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | auto-deprecate: 9 | name: NPM Auto Deprecate 10 | uses: sapphiredev/.github/.github/workflows/reusable-yarn-job.yml@main 11 | with: 12 | script-name: npm-deprecate 13 | secrets: 14 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 15 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | PublishPackage: 8 | name: Publish @sapphire/cli 9 | uses: sapphiredev/.github/.github/workflows/reusable-publish.yml@main 10 | with: 11 | project-name: '@sapphire/cli' 12 | secrets: 13 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 14 | SKYRA_TOKEN: ${{ secrets.SKYRA_TOKEN }} 15 | -------------------------------------------------------------------------------- /templates/.sapphirerc.yml.sapphire: -------------------------------------------------------------------------------- 1 | $schema: "./node_modules/@sapphire/cli/templates/schemas/.sapphirerc.scheme.json" 2 | projectLanguage: "{{language}}" 3 | locations: 4 | base: src 5 | arguments: arguments 6 | commands: commands 7 | listeners: listeners 8 | preconditions: preconditions 9 | interaction-handlers: interaction-handlers 10 | routes: routes 11 | customFileTemplates: 12 | enabled: false 13 | location: "" 14 | -------------------------------------------------------------------------------- /templates/components/listener.js.sapphire: -------------------------------------------------------------------------------- 1 | { "category": "listeners" } 2 | --- 3 | const { Listener } = require('@sapphire/framework'); 4 | 5 | class UserEvent extends Listener { 6 | /** 7 | * @param {Listener.LoaderContext} context 8 | */ 9 | constructor(context) { 10 | super(context, { 11 | // Any Listener options you want here 12 | }); 13 | } 14 | 15 | run() {} 16 | } 17 | 18 | module.exports = { 19 | UserEvent 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore a blackhole and the folder for development 2 | node_modules/ 3 | .vs/ 4 | .idea/ 5 | *.iml 6 | coverage/ 7 | docs/ 8 | 9 | # Yarn files 10 | .yarn/install-state.gz 11 | .yarn/build-state.yml 12 | 13 | # Ignore generated files 14 | dist/ 15 | 16 | # Ignore heapsnapshot and log files 17 | *.heapsnapshot 18 | *.log 19 | 20 | # Ignore package locks 21 | package-lock.json 22 | 23 | # Ignore the GH cli downloaded by workflows 24 | gh 25 | -------------------------------------------------------------------------------- /src/lib/aliases.ts: -------------------------------------------------------------------------------- 1 | export const commandNames = ['command', 'commands']; 2 | export const componentCommandNames = ['messagecommand', 'slashcommand', 'contextmenucommand']; 3 | 4 | export const interactionHandlerNames = ['interactionhandler', 'interactionhandlers']; 5 | export const componentInteractionHandlerNames = [ 6 | 'buttoninteractionhandler', 7 | 'autocompleteinteractionhandler', 8 | 'modalinteractionhandler', 9 | 'selectmenuinteractionhandler' 10 | ]; 11 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@sapphire/ts-config", "@sapphire/ts-config/extra-strict", "@sapphire/ts-config/verbatim"], 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "paths": { 6 | "#prompts/*": ["src/prompts/*.ts"], 7 | "#lib/*": ["src/lib/*.ts"], 8 | "#functions/*": ["src/functions/*.ts"], 9 | "#commands/*": ["src/commands/*.ts"], 10 | "#constants": ["src/constants.ts"] 11 | }, 12 | "moduleResolution": "node16" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.{js,ts}] 10 | indent_size = 4 11 | indent_style = tab 12 | block_comment_start = /* 13 | block_comment = * 14 | block_comment_end = */ 15 | 16 | [*.{yml,yaml}] 17 | indent_size = 2 18 | indent_style = space 19 | 20 | [*.{md,rmd,mkd,mkdn,mdwn,mdown,markdown,litcoffee}] 21 | tab_width = 4 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /.github/workflows/deprecate-on-merge.yml: -------------------------------------------------------------------------------- 1 | name: NPM Deprecate PR versions On Merge 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - closed 7 | 8 | jobs: 9 | deprecate-on-merge: 10 | name: NPM Deprecate PR versions On Merge 11 | uses: sapphiredev/.github/.github/workflows/reusable-yarn-job.yml@main 12 | with: 13 | script-name: npm-deprecate --name "*pr-${{ github.event.number }}*" -d -v 14 | secrets: 15 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 16 | -------------------------------------------------------------------------------- /templates/components/argument.ts.sapphire: -------------------------------------------------------------------------------- 1 | { "category": "arguments" } 2 | --- 3 | import { ApplyOptions } from '@sapphire/decorators'; 4 | import { Argument } from '@sapphire/framework'; 5 | 6 | @ApplyOptions({}) 7 | export class UserArgument extends Argument { 8 | public override run(parameter: string) { 9 | return this.ok(parameter); 10 | } 11 | } 12 | 13 | declare module '@sapphire/framework' { 14 | interface ArgType { 15 | {{name}}: string; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /templates/components/messagecommand.ts.sapphire: -------------------------------------------------------------------------------- 1 | { "category": "commands" } 2 | --- 3 | import { ApplyOptions } from '@sapphire/decorators'; 4 | import { Command } from '@sapphire/framework'; 5 | import type { Message } from 'discord.js'; 6 | 7 | @ApplyOptions({ 8 | description: 'A basic command' 9 | }) 10 | export class UserCommand extends Command { 11 | public override async messageRun(message: Message) { 12 | return message.channel.send('Hello world!'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /templates/components/argument.js.sapphire: -------------------------------------------------------------------------------- 1 | { "category": "arguments" } 2 | --- 3 | const { Argument } = require('@sapphire/framework'); 4 | 5 | class UserArgument extends Argument { 6 | /** 7 | * @param {Argument.LoaderContext} context 8 | */ 9 | constructor(context) { 10 | super(context, { 11 | // Any Argument options you want here 12 | }); 13 | } 14 | 15 | async run(parameter) { 16 | return this.ok(parameter); 17 | } 18 | } 19 | 20 | module.exports = { 21 | UserArgument 22 | }; 23 | -------------------------------------------------------------------------------- /templates/.sapphirerc.json.sapphire: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/sapphiredev/cli/main/templates/schemas/.sapphirerc.scheme.json", 3 | "projectLanguage": "{{language}}", 4 | "locations": { 5 | "base": "src", 6 | "arguments": "arguments", 7 | "commands": "commands", 8 | "listeners": "listeners", 9 | "preconditions": "preconditions", 10 | "interaction-handlers": "interaction-handlers", 11 | "routes": "routes" 12 | }, 13 | "customFileTemplates": { 14 | "enabled": false, 15 | "location": "" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.eol": "\n", 3 | "search.exclude": { 4 | "**/node_modules": true, 5 | "**/bower_components": true, 6 | "**/*.code-search": true, 7 | "**/.yarn": true, 8 | "**/dist/": true, 9 | "**/.git/": true 10 | }, 11 | "files.associations": { 12 | "*.js.sapphire": "javascript", 13 | "*.ts.sapphire": "typescript", 14 | "*.json.sapphire": "json", 15 | "*.yml.sapphire": "yaml" 16 | }, 17 | "sonarlint.connectedMode.project": { 18 | "connectionId": "sapphiredev", 19 | "projectKey": "sapphiredev_cli" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { URL, fileURLToPath } from 'url'; 2 | 3 | const rootURL = new URL('../', import.meta.url); 4 | const templatesURL = new URL('./templates/', rootURL); 5 | const componentsURL = new URL('./components/', templatesURL); 6 | 7 | export const rootFolder = fileURLToPath(rootURL); 8 | export const templatesFolder = fileURLToPath(templatesURL); 9 | export const componentsFolder = fileURLToPath(componentsURL); 10 | export const repoUrl = 'https://github.com/sapphiredev/examples.git'; 11 | export const locationReplacement = '{{LOCATION_REPLACEMENT}}'; 12 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | linting: 11 | name: Linting 12 | uses: sapphiredev/.github/.github/workflows/reusable-lint.yml@main 13 | 14 | build: 15 | name: Building 16 | uses: sapphiredev/.github/.github/workflows/reusable-build.yml@main 17 | 18 | sonar: 19 | name: Sonar Analysis 20 | uses: sapphiredev/.github/.github/workflows/reusable-sonar.yml@main 21 | secrets: 22 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 23 | -------------------------------------------------------------------------------- /templates/components/messagecommand.js.sapphire: -------------------------------------------------------------------------------- 1 | { "category": "commands" } 2 | --- 3 | const { Command } = require('@sapphire/framework'); 4 | 5 | class UserCommand extends Command { 6 | /** 7 | * @param {Command.LoaderContext} context 8 | */ 9 | constructor(context) { 10 | super(context, { 11 | // Any Command options you want here 12 | }); 13 | } 14 | 15 | /** 16 | * @param {import('discord.js').Message} message 17 | */ 18 | async messageRun(message) { 19 | return message.channel.send('Hello world!'); 20 | } 21 | } 22 | 23 | module.exports = { 24 | UserCommand 25 | }; 26 | -------------------------------------------------------------------------------- /templates/components/route.ts.sapphire: -------------------------------------------------------------------------------- 1 | { "category": "routes" } 2 | --- 3 | import { ApplyOptions } from '@sapphire/decorators'; 4 | import { methods, Route, type ApiRequest, type ApiResponse } from '@sapphire/plugin-api'; 5 | 6 | @ApplyOptions({ 7 | route: 'hello-world' 8 | }) 9 | export class UserRoute extends Route { 10 | public [methods.GET](_request: ApiRequest, response: ApiResponse) { 11 | response.json({ message: 'Hello World' }); 12 | } 13 | 14 | public [methods.POST](_request: ApiRequest, response: ApiResponse) { 15 | response.json({ message: 'Hello World' }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /templates/components/precondition.js.sapphire: -------------------------------------------------------------------------------- 1 | { "category": "preconditions" } 2 | --- 3 | const { Precondition } = require('@sapphire/framework'); 4 | 5 | class UserPrecondition extends Precondition { 6 | /** 7 | * @param {import('discord.js').Message} message 8 | */ 9 | messageRun(message) { 10 | return this.ok(); 11 | } 12 | 13 | /** 14 | * @param {import('discord.js').ChatInputCommandInteraction} interaction 15 | */ 16 | chatInputRun(interaction) { 17 | return this.ok(); 18 | } 19 | 20 | /** 21 | * @param {import('discord.js').ContextMenuCommandInteraction} interaction 22 | */ 23 | contextMenuRun(interaction) { 24 | return this.ok(); 25 | } 26 | } 27 | 28 | module.exports = { 29 | UserPrecondition 30 | }; 31 | -------------------------------------------------------------------------------- /templates/components/slashcommand.ts.sapphire: -------------------------------------------------------------------------------- 1 | { "category": "commands" } 2 | --- 3 | import { ApplyOptions } from '@sapphire/decorators'; 4 | import { Command } from '@sapphire/framework'; 5 | 6 | @ApplyOptions({ 7 | description: 'A basic slash command' 8 | }) 9 | export class UserCommand extends Command { 10 | public override registerApplicationCommands(registry: Command.Registry) { 11 | registry.registerChatInputCommand((builder) => 12 | builder // 13 | .setName(this.name) 14 | .setDescription(this.description) 15 | ); 16 | } 17 | 18 | public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { 19 | return interaction.reply({ content: 'Hello world!' }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /templates/components/precondition.ts.sapphire: -------------------------------------------------------------------------------- 1 | { "category": "preconditions" } 2 | --- 3 | import { Precondition } from '@sapphire/framework'; 4 | import type { ChatInputCommandInteraction, ContextMenuCommandInteraction, Message } from 'discord.js'; 5 | 6 | export class UserPrecondition extends Precondition { 7 | public override messageRun(message: Message) { 8 | return this.ok(); 9 | } 10 | 11 | public override chatInputRun(interaction: ChatInputCommandInteraction) { 12 | return this.ok(); 13 | } 14 | 15 | public override contextMenuRun(interaction: ContextMenuCommandInteraction) { 16 | return this.ok(); 17 | } 18 | } 19 | 20 | declare module '@sapphire/framework' { 21 | interface Preconditions { 22 | {{name}}: never; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /templates/components/contextmenucommand.ts.sapphire: -------------------------------------------------------------------------------- 1 | { "category": "commands" } 2 | --- 3 | import { ApplyOptions } from '@sapphire/decorators'; 4 | import { Command } from '@sapphire/framework'; 5 | import { ApplicationCommandType } from 'discord.js'; 6 | 7 | @ApplyOptions({ 8 | description: 'A basic contextMenu command' 9 | }) 10 | export class UserCommand extends Command { 11 | public override registerApplicationCommands(registry: Command.Registry) { 12 | registry.registerContextMenuCommand((builder) => 13 | builder // 14 | .setName(this.name) 15 | .setType(ApplicationCommandType.Message) 16 | ); 17 | } 18 | 19 | public override async contextMenuRun(interaction: Command.ContextMenuCommandInteraction) { 20 | return interaction.reply({ content: 'Hello world!' }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /templates/components/modalinteractionhandler.ts.sapphire: -------------------------------------------------------------------------------- 1 | { "category": "interaction-handlers" } 2 | --- 3 | import { ApplyOptions } from '@sapphire/decorators'; 4 | import { InteractionHandler, InteractionHandlerTypes } from '@sapphire/framework'; 5 | import type { ModalSubmitInteraction } from 'discord.js'; 6 | 7 | @ApplyOptions({ 8 | interactionHandlerType: InteractionHandlerTypes.ModalSubmit 9 | }) 10 | export class ModalHandler extends InteractionHandler { 11 | public async run(interaction: ModalSubmitInteraction) { 12 | await interaction.reply({ 13 | content: 'Thank you for submitting the form!', 14 | ephemeral: true 15 | }); 16 | } 17 | 18 | public override parse(interaction: ModalSubmitInteraction) { 19 | if (interaction.customId !== 'hello-popup') return this.none(); 20 | 21 | return this.some(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /templates/components/buttoninteractionhandler.ts.sapphire: -------------------------------------------------------------------------------- 1 | { "category": "interaction-handlers" } 2 | --- 3 | import { ApplyOptions } from '@sapphire/decorators'; 4 | import { InteractionHandler, InteractionHandlerTypes } from '@sapphire/framework'; 5 | import type { ButtonInteraction } from 'discord.js'; 6 | 7 | @ApplyOptions({ 8 | interactionHandlerType: InteractionHandlerTypes.Button 9 | }) 10 | export class ButtonHandler extends InteractionHandler { 11 | public async run(interaction: ButtonInteraction) { 12 | await interaction.reply({ 13 | content: 'Hello from a button interaction handler!', 14 | // Let's make it so only the person who pressed the button can see this message! 15 | ephemeral: true 16 | }); 17 | } 18 | 19 | public override parse(interaction: ButtonInteraction) { 20 | if (interaction.customId !== 'my-awesome-button') return this.none(); 21 | 22 | return this.some(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /templates/components/selectmenuinteractionhandler.ts.sapphire: -------------------------------------------------------------------------------- 1 | { "category": "interaction-handlers" } 2 | --- 3 | import { ApplyOptions } from '@sapphire/decorators'; 4 | import { InteractionHandler, InteractionHandlerTypes } from '@sapphire/framework'; 5 | import type { StringSelectMenuInteraction } from 'discord.js'; 6 | 7 | @ApplyOptions({ 8 | interactionHandlerType: InteractionHandlerTypes.SelectMenu 9 | }) 10 | export class MenuHandler extends InteractionHandler { 11 | public override async run(interaction: StringSelectMenuInteraction) { 12 | await interaction.reply({ 13 | // Remember how we can have multiple values? Let's get the first one! 14 | content: `You selected: ${interaction.values[0]}` 15 | }); 16 | } 17 | 18 | public override parse(interaction: StringSelectMenuInteraction) { 19 | if (interaction.customId !== 'my-echo-select') return this.none(); 20 | 21 | return this.some(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /templates/components/route.js.sapphire: -------------------------------------------------------------------------------- 1 | { "category": "routes" } 2 | --- 3 | const { methods, Route } = require('@sapphire/plugin-api'); 4 | 5 | class UserRoute extends Route { 6 | /** 7 | * @param {Route.LoaderContext} context 8 | * @param {Route.Options} options 9 | */ 10 | constructor(context, options) { 11 | super(context, { 12 | ...options, 13 | route: 'hello-world' 14 | }); 15 | } 16 | /** 17 | * @param {import('@sapphire/plugin-api').ApiRequest} request 18 | * @param {import('@sapphire/plugin-api').ApiResponse} response 19 | */ 20 | [methods.GET](request, response) { 21 | response.json({ message: 'Hello World' }); 22 | } 23 | /** 24 | * @param {import('@sapphire/plugin-api').ApiRequest} request 25 | * @param {import('@sapphire/plugin-api').ApiResponse} response 26 | */ 27 | [methods.POST](request, response) { 28 | response.json({ message: 'Hello World' }); 29 | } 30 | } 31 | module.exports = { 32 | UserRoute 33 | }; 34 | -------------------------------------------------------------------------------- /templates/components/slashcommand.js.sapphire: -------------------------------------------------------------------------------- 1 | { "category": "commands" } 2 | --- 3 | const { Command } = require('@sapphire/framework') 4 | 5 | class UserCommand extends Command { 6 | /** 7 | * @param {Command.LoaderContext} context 8 | */ 9 | constructor(context) { 10 | super(context, { 11 | // Any Command options you want here 12 | name: 'command', 13 | description: 'A basic slash command' 14 | }); 15 | } 16 | 17 | /** 18 | * @param {Command.Registry} registry 19 | */ 20 | registerApplicationCommands(registry) { 21 | registry.registerChatInputCommand( 22 | (builder) => 23 | builder // 24 | .setName(this.name) 25 | .setDescription(this.description) 26 | ); 27 | } 28 | 29 | /** 30 | * @param {Command.ChatInputCommandInteraction} interaction 31 | */ 32 | async chatInputRun(interaction) { 33 | return interaction.reply({ content: 'Hello world!' }); 34 | } 35 | } 36 | 37 | module.exports = { 38 | UserCommand 39 | } 40 | -------------------------------------------------------------------------------- /templates/components/contextmenucommand.js.sapphire: -------------------------------------------------------------------------------- 1 | { "category": "commands" } 2 | --- 3 | const { Command } = require('@sapphire/framework'); 4 | const { ApplicationCommandType } = require('discord.js'); 5 | 6 | class UserCommand extends Command { 7 | /** 8 | * @param {Command.LoaderContext} context 9 | */ 10 | constructor(context) { 11 | super(context, { 12 | // Any Command options you want here 13 | name: 'command' 14 | }); 15 | } 16 | 17 | /** 18 | * @param {Command.Registry} registry 19 | */ 20 | registerApplicationCommands(registry) { 21 | registry.registerContextMenuCommand((builder) => 22 | builder // 23 | .setName(this.name) 24 | .setType(ApplicationCommandType.Message) 25 | ); 26 | } 27 | 28 | /** 29 | * @param {Command.ContextMenuCommandInteraction} interaction 30 | */ 31 | async contextMenuRun(interaction) { 32 | return interaction.reply({ content: 'Hello world!' }); 33 | } 34 | } 35 | 36 | module.exports = { 37 | UserCommand 38 | }; 39 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Scheme for Sapphire CLI Config (@sapphire/cli) 3 | */ 4 | export interface Config { 5 | /** 6 | * Settings about custom component (piece) templates 7 | */ 8 | customFileTemplates: CustomFileTemplates; 9 | /** 10 | * Categories and their locations 11 | */ 12 | locations: Locations; 13 | /** 14 | * Project language (ts | js) 15 | */ 16 | projectLanguage: string; 17 | [property: string]: any; 18 | } 19 | 20 | /** 21 | * Settings about custom component (piece) templates 22 | */ 23 | interface CustomFileTemplates { 24 | /** 25 | * Enable custom file templates 26 | */ 27 | enabled: boolean; 28 | /** 29 | * Location of your custom file templates 30 | */ 31 | location: string; 32 | [property: string]: any; 33 | } 34 | 35 | /** 36 | * Categories and their locations 37 | */ 38 | interface Locations { 39 | arguments: string; 40 | base: string; 41 | commands: string; 42 | listeners: string; 43 | preconditions: string; 44 | routes?: string; 45 | [property: string]: any; 46 | } 47 | -------------------------------------------------------------------------------- /templates/components/modalinteractionhandler.js.sapphire: -------------------------------------------------------------------------------- 1 | { "category": "interaction-handlers" } 2 | --- 3 | const { InteractionHandler, InteractionHandlerTypes } = require('@sapphire/framework'); 4 | 5 | class ModalHandler extends InteractionHandler { 6 | /** 7 | * @param {InteractionHandler.LoaderContext} context 8 | * @param {InteractionHandler.Options} options 9 | */ 10 | constructor(context, options) { 11 | super(context, { 12 | ...options, 13 | interactionHandlerType: InteractionHandlerTypes.ModalSubmit 14 | }); 15 | } 16 | 17 | /** 18 | * @param {import('discord.js').ModalSubmitInteraction} interaction 19 | */ 20 | async run(interaction) { 21 | await interaction.reply({ 22 | content: 'Thank you for submitting the form!', 23 | ephemeral: true 24 | }); 25 | } 26 | 27 | /** 28 | * @param {import('discord.js').ModalSubmitInteraction} interaction 29 | */ 30 | parse(interaction) { 31 | if (interaction.customId !== 'hello-popup') return this.none(); 32 | 33 | return this.some(); 34 | } 35 | } 36 | 37 | module.exports = { 38 | ModalHandler 39 | }; 40 | -------------------------------------------------------------------------------- /.github/workflows/continuous-delivery.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Delivery 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | prNumber: 7 | description: The number of the PR that is being deployed 8 | required: false 9 | type: string 10 | ref: 11 | description: The branch that is being deployed. Should be a branch on the given repository 12 | required: false 13 | default: main 14 | type: string 15 | repository: 16 | description: The {owner}/{repository} that is being deployed. 17 | required: false 18 | default: sapphiredev/cli 19 | type: string 20 | push: 21 | branches: 22 | - main 23 | 24 | jobs: 25 | Publish: 26 | name: Publish Next to npm 27 | uses: sapphiredev/.github/.github/workflows/reusable-continuous-delivery.yml@main 28 | with: 29 | pr-number: ${{ github.event.inputs.prNumber }} 30 | ref: ${{ github.event.inputs.ref }} 31 | repository: ${{ github.event.inputs.repository }} 32 | secrets: 33 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 34 | -------------------------------------------------------------------------------- /templates/components/selectmenuinteractionhandler.js.sapphire: -------------------------------------------------------------------------------- 1 | { "category": "interaction-handlers" } 2 | --- 3 | const { InteractionHandler, InteractionHandlerTypes } = require('@sapphire/framework'); 4 | 5 | class MenuHandler extends InteractionHandler { 6 | /** 7 | * @param {InteractionHandler.LoaderContext} context 8 | * @param {InteractionHandler.Options} options 9 | */ 10 | constructor(context, options) { 11 | super(context, { 12 | ...options, 13 | interactionHandlerType: InteractionHandlerTypes.SelectMenu 14 | }); 15 | } 16 | 17 | /** 18 | * @param {import('discord.js').StringSelectMenuInteraction} interaction 19 | */ 20 | async run(interaction) { 21 | await interaction.reply({ 22 | // Remember how we can have multiple values? Let's get the first one! 23 | content: `You selected: ${interaction.values[0]}` 24 | }); 25 | } 26 | 27 | /** 28 | * @param {import('discord.js').StringSelectMenuInteraction} interaction 29 | */ 30 | parse(interaction) { 31 | if (interaction.customId !== 'my-echo-select') return this.none(); 32 | 33 | return this.some(); 34 | } 35 | } 36 | 37 | module.exports = { 38 | MenuHandler 39 | }; 40 | -------------------------------------------------------------------------------- /templates/components/buttoninteractionhandler.js.sapphire: -------------------------------------------------------------------------------- 1 | { "category": "interaction-handlers" } 2 | --- 3 | const { InteractionHandler, InteractionHandlerTypes } = require('@sapphire/framework'); 4 | 5 | class ButtonHandler extends InteractionHandler { 6 | /** 7 | * @param {InteractionHandler.LoaderContext} context 8 | * @param {InteractionHandler.Options} options 9 | */ 10 | constructor(context, options) { 11 | super(context, { 12 | ...options, 13 | interactionHandlerType: InteractionHandlerTypes.Button 14 | }); 15 | } 16 | 17 | /** 18 | * @param {import('discord.js').ButtonInteraction} interaction 19 | */ 20 | async run(interaction) { 21 | await interaction.reply({ 22 | content: 'Hello from a button interaction handler!', 23 | // Let's make it so only the person who pressed the button can see this message! 24 | ephemeral: true 25 | }); 26 | } 27 | 28 | /** 29 | * @param {import('discord.js').ButtonInteraction} interaction 30 | */ 31 | parse(interaction) { 32 | if (interaction.customId !== 'my-awesome-button') return this.none(); 33 | return this.some(); 34 | } 35 | } 36 | 37 | module.exports = { 38 | ButtonHandler 39 | }; 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright © `2021` `The Sapphire Community and its contributors` 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the “Software”), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /src/commands/generate-loader.ts: -------------------------------------------------------------------------------- 1 | import { locationReplacement } from '#constants'; 2 | import { CreateComponentLoaders } from '#functions/CreateComponentLoader'; 3 | import { generateCommandFlow } from '#functions/generateCommandFlow'; 4 | import type { Config } from '#lib/types'; 5 | import { join } from 'node:path'; 6 | 7 | /** 8 | * Generates loaders based on the Sapphire CLI config. 9 | * @returns A promise that resolves when the loaders are created. 10 | */ 11 | export default async (): Promise => { 12 | return generateCommandFlow('Creating loaders...', (config, configLocation) => createLoader(config, configLocation)); 13 | }; 14 | 15 | /** 16 | * Creates a loader component based on the provided configuration. 17 | * @param config - The configuration object. 18 | * @param configLoc - The location of the configuration file. 19 | * @returns A promise that resolves to the created loader component. 20 | * @throws An error if the 'projectLanguage' field is missing in the configuration file or if a template file for the loader component cannot be found. 21 | */ 22 | export async function createLoader(config: Config, configLoc: string) { 23 | const { projectLanguage } = config; 24 | 25 | if (!projectLanguage) { 26 | throw new Error("There is no 'projectLanguage' field in .sapphirerc.json"); 27 | } 28 | 29 | const targetDir = join(configLoc, config.locations.base, locationReplacement); 30 | 31 | return CreateComponentLoaders(targetDir, config); 32 | } 33 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import generateCmd from '#commands/generate'; 4 | import generateLoaderCmd from '#commands/generate-loader'; 5 | import initCmd from '#commands/init'; 6 | import newCmd from '#commands/new'; 7 | import { createColors } from 'colorette'; 8 | import { Command } from 'commander'; 9 | import { readFile } from 'node:fs/promises'; 10 | import { URL } from 'node:url'; 11 | 12 | createColors({ useColor: true }); 13 | 14 | const sapphire = new Command(); 15 | 16 | const packageFile = new URL('../package.json', import.meta.url); 17 | const packageJson = JSON.parse(await readFile(packageFile, 'utf-8')); 18 | 19 | sapphire // 20 | .name('sapphire') 21 | .version(packageJson.version); 22 | 23 | sapphire 24 | .command('new') 25 | .description('creates a new Sapphire project') 26 | .alias('n') 27 | .argument('[name]', 'project name') 28 | .option('-v, --verbose') 29 | .action(newCmd); 30 | 31 | sapphire 32 | .command('generate') 33 | .description('generates a component/piece') 34 | .alias('g') 35 | .argument('', 'component/piece name') 36 | .argument('', 'file name') 37 | .action(generateCmd); 38 | 39 | sapphire // 40 | .command('generate-loader') 41 | .description('generates a piece loader') 42 | .alias('gl') 43 | .action(generateLoaderCmd); 44 | 45 | sapphire // 46 | .command('init') 47 | .description('creates a config file on an existing Sapphire project') 48 | .alias('i') 49 | .action(initCmd); 50 | 51 | sapphire.parse(process.argv); 52 | -------------------------------------------------------------------------------- /templates/components/autocompleteinteractionhandler.ts.sapphire: -------------------------------------------------------------------------------- 1 | { "category": "interaction-handlers" } 2 | --- 3 | import { ApplyOptions } from '@sapphire/decorators'; 4 | import { InteractionHandler, InteractionHandlerTypes } from '@sapphire/framework'; 5 | import { AutocompleteInteraction, type ApplicationCommandOptionChoiceData } from 'discord.js'; 6 | 7 | @ApplyOptions({ 8 | interactionHandlerType: InteractionHandlerTypes.Autocomplete 9 | }) 10 | export class AutocompleteHandler extends InteractionHandler { 11 | public override async run(interaction: AutocompleteInteraction, result: ApplicationCommandOptionChoiceData[]) { 12 | return interaction.respond(result); 13 | } 14 | 15 | public override async parse(interaction: AutocompleteInteraction) { 16 | // Only run this interaction for the command with ID '1000000000000000000' 17 | if (interaction.commandId !== '1000000000000000000') return this.none(); 18 | // Get the focussed (current) option 19 | const focusedOption = interaction.options.getFocused(true); 20 | // Ensure that the option name is one that can be autocompleted, or return none if not. 21 | switch (focusedOption.name) { 22 | case 'search': { 23 | // Search your API or similar. This is example code! 24 | const searchResult = await myApi.searchForSomething(focusedOption.value); 25 | // Map the search results to the structure required for Autocomplete 26 | return this.some(searchResult.map((match) => ({ name: match.name, value: match.key }))); 27 | } 28 | default: 29 | return this.none(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/commands/init.ts: -------------------------------------------------------------------------------- 1 | import { PromptInit, type PromptInitObjectKeys } from '#prompts/PromptInit'; 2 | import { red } from 'colorette'; 3 | import findUp from 'find-up'; 4 | import { dump } from 'js-yaml'; 5 | import { writeFile } from 'node:fs/promises'; 6 | import prompts from 'prompts'; 7 | 8 | /** 9 | * Initializes the project by prompting the user for configuration options and generating a configuration file. 10 | * @returns A promise that resolves when the initialization is complete. 11 | */ 12 | export default async (): Promise => { 13 | const packageJson = await findUp('package.json'); 14 | 15 | if (!packageJson) { 16 | console.log(red("Can't find package.json")); 17 | process.exit(1); 18 | } 19 | 20 | const response = await prompts(PromptInit); 21 | if (!response.preconditions) process.exit(1); 22 | 23 | const config = { 24 | projectLanguage: response.projectLanguage, 25 | locations: { 26 | base: response.base, 27 | arguments: response.arguments, 28 | commands: response.commands, 29 | listeners: response.listeners, 30 | preconditions: response.preconditions, 31 | 'interaction-handlers': response['interaction-handlers'], 32 | routes: response.routes ?? '' 33 | }, 34 | customFileTemplates: { 35 | enabled: response.cftEnabled, 36 | location: response.cftLocation ?? '' 37 | } 38 | }; 39 | 40 | const file = response.configFormat === 'json' ? JSON.stringify(config, null, 2) : dump(config); 41 | await writeFile(packageJson.replace('package.json', `.sapphirerc.${response.configFormat}`), file); 42 | process.exit(0); 43 | }; 44 | -------------------------------------------------------------------------------- /templates/schemas/.sapphirerc.scheme.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft-04/schema#", 3 | "title": "Sapphire CLI Config", 4 | "description": "Scheme for Sapphire CLI Config (@sapphire/cli)", 5 | "type": "object", 6 | "properties": { 7 | "projectLanguage": { 8 | "description": "Project language (ts | js)", 9 | "type": "string", 10 | "enum": ["ts", "js"] 11 | }, 12 | "locations": { 13 | "description": "Categories and their locations", 14 | "type": "object", 15 | "properties": { 16 | "base": { 17 | "type": "string" 18 | }, 19 | "arguments": { 20 | "type": "string" 21 | }, 22 | "commands": { 23 | "type": "string" 24 | }, 25 | "listeners": { 26 | "type": "string" 27 | }, 28 | "preconditions": { 29 | "type": "string" 30 | }, 31 | "interaction-handlers": { 32 | "type": "string" 33 | }, 34 | "routes": { 35 | "type": "string" 36 | } 37 | }, 38 | "required": ["base", "arguments", "commands", "listeners", "preconditions", "interaction-handlers"] 39 | }, 40 | "customFileTemplates": { 41 | "description": "Settings about custom component (piece) templates", 42 | "type": "object", 43 | "properties": { 44 | "enabled": { 45 | "description": "Enable custom file templates", 46 | "type": "boolean" 47 | }, 48 | "location": { 49 | "description": "Location of your custom file templates", 50 | "type": "string" 51 | } 52 | }, 53 | "required": ["enabled", "location"] 54 | } 55 | }, 56 | "required": ["projectLanguage", "locations", "customFileTemplates"] 57 | } 58 | -------------------------------------------------------------------------------- /templates/components/autocompleteinteractionhandler.js.sapphire: -------------------------------------------------------------------------------- 1 | { "category": "interaction-handlers" } 2 | --- 3 | const { InteractionHandler, InteractionHandlerTypes } = require('@sapphire/framework'); 4 | const { InteractionHandler, InteractionHandlerTypes } = require('@sapphire/framework'); 5 | 6 | class AutocompleteHandler extends InteractionHandler { 7 | /** 8 | * @param {InteractionHandler.LoaderContext} context 9 | * @param {InteractionHandler.Options} options 10 | */ 11 | constructor(context, options) { 12 | super(context, { 13 | ...options, 14 | interactionHandlerType: InteractionHandlerTypes.Autocomplete 15 | }); 16 | } 17 | 18 | /** 19 | * @param {import('discord.js').AutocompleteInteraction} interaction 20 | * @param {import('discord.js').ApplicationCommandOptionChoiceData[]} result 21 | */ 22 | async run(interaction, result) { 23 | return interaction.respond(result); 24 | } 25 | 26 | /** 27 | * @param {import('discord.js').AutocompleteInteraction} interaction 28 | */ 29 | async parse(interaction) { 30 | // Only run this interaction for the command with ID '1000000000000000000' 31 | if (interaction.commandId !== '1000000000000000000') return this.none(); 32 | // Get the focussed (current) option 33 | const focusedOption = interaction.options.getFocused(true); 34 | // Ensure that the option name is one that can be autocompleted, or return none if not. 35 | switch (focusedOption.name) { 36 | case 'search': { 37 | // Search your API or similar. This is example code! 38 | const searchResult = await myApi.searchForSomething(focusedOption.value); 39 | // Map the search results to the structure required for Autocomplete 40 | return this.some(searchResult.map((match) => ({ name: match.name, value: match.key }))); 41 | } 42 | default: 43 | return this.none(); 44 | } 45 | } 46 | } 47 | 48 | module.exports = { 49 | AutocompleteHandler 50 | }; 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ![Sapphire Logo](https://raw.githubusercontent.com/sapphiredev/assets/main/banners/SapphireCommunity.png) 4 | 5 | # @sapphire/cli 6 | 7 | **CLI for Sapphire Framework.** 8 | 9 | [![GitHub](https://img.shields.io/github/license/sapphiredev/cli?style=flat-square)](https://github.com/sapphiredev/cli/blob/main/LICENSE.md) 10 | [![npm](https://img.shields.io/npm/v/@sapphire/cli?color=crimson&logo=npm&style=flat-square)](https://www.npmjs.com/package/@sapphire/cli) 11 | 12 |
13 | 14 | ## Features 15 | 16 | - Written in TypeScript 17 | - Generate Sapphire projects easily 18 | - Generate components (commands, listeners, etc.) 19 | - Create your own templates for components 20 | - Generate loaders for [virtual components](https://www.sapphirejs.dev/docs/Guide/additional-information/registering-virtual-pieces) 21 | 22 | ## [Get Started with the CLI](https://www.sapphirejs.dev/docs/Guide/CLI/introduction) 23 | 24 | ## Buy us some doughnuts 25 | 26 | Sapphire Community is and always will be open source, even if we don't get donations. That being said, we know there are amazing people who may still want to donate just to show their appreciation. Thank you very much in advance! 27 | 28 | We accept donations through Open Collective, Ko-fi, Paypal, Patreon and GitHub Sponsorships. You can use the buttons below to donate through your method of choice. 29 | 30 | | Donate With | Address | 31 | | :-------------: | :-------------------------------------------------: | 32 | | Open Collective | [Click Here](https://sapphirejs.dev/opencollective) | 33 | | Ko-fi | [Click Here](https://sapphirejs.dev/kofi) | 34 | | Patreon | [Click Here](https://sapphirejs.dev/patreon) | 35 | | PayPal | [Click Here](https://sapphirejs.dev/paypal) | 36 | 37 | ## Contributors 38 | 39 | Please make sure to read the [Contributing Guide][contributing] before making a pull request. 40 | 41 | Thank you to all the people who already contributed to Sapphire! 42 | 43 | 44 | 45 | 46 | 47 | [contributing]: https://github.com/sapphiredev/.github/blob/main/.github/CONTRIBUTING.md 48 | -------------------------------------------------------------------------------- /src/functions/generateCommandFlow.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '#lib/types'; 2 | import { Spinner } from '@favware/colorette-spinner'; 3 | import { Result } from '@sapphire/result'; 4 | import { blueBright, red } from 'colorette'; 5 | import findUp from 'find-up'; 6 | import { load } from 'js-yaml'; 7 | import { readFile } from 'node:fs/promises'; 8 | import { setTimeout as sleep } from 'node:timers/promises'; 9 | 10 | export async function generateCommandFlow(spinnerMessage: string, callback: (config: Config, configLocation: string) => Promise) { 11 | const spinner = new Spinner(spinnerMessage).start(); 12 | 13 | const fail = (error: string, additionalExecution?: () => void) => { 14 | spinner.error({ text: error }); 15 | additionalExecution?.(); 16 | process.exit(1); 17 | }; 18 | 19 | const configLocation = await fetchConfig(); 20 | 21 | if (!configLocation) { 22 | return fail("Can't find the Sapphire CLI config."); 23 | } 24 | 25 | const config: Config = configLocation.endsWith('json') 26 | ? JSON.parse(await readFile(configLocation, 'utf8')) 27 | : load(await readFile(configLocation, 'utf8')); 28 | 29 | if (!config) { 30 | return fail("Can't parse the Sapphire CLI config."); 31 | } 32 | 33 | const result = await Result.fromAsync(() => callback(config, configLocation.replace(/.sapphirerc.(json|yml)/g, ''))); 34 | 35 | return result.match({ 36 | ok: () => { 37 | spinner.success(); 38 | 39 | console.log(blueBright('Done!')); 40 | process.exit(0); 41 | }, 42 | err: (error) => fail(error.message, () => console.log(red(error.message))) 43 | }); 44 | } 45 | 46 | /** 47 | * Fetches the configuration file asynchronously. 48 | * It first tries to find the '.sapphirerc.json' file in the current working directory. 49 | * If not found, it then tries to find the '.sapphirerc.yml' file. 50 | * Returns a Promise that resolves to the configuration file as JSON if found, otherwise resolves to null. 51 | * @returns A Promise that resolves to the configuration file as JSON if found, otherwise resolves to null. 52 | */ 53 | async function fetchConfig() { 54 | const configFileAsJson = await Promise.race([findUp('.sapphirerc.json', { cwd: '.' }), sleep(5000)]); 55 | 56 | if (configFileAsJson) { 57 | return configFileAsJson; 58 | } 59 | 60 | return Promise.race([findUp('.sapphirerc.yml', { cwd: '.' }), sleep(5000)]); 61 | } 62 | -------------------------------------------------------------------------------- /src/prompts/PromptInit.ts: -------------------------------------------------------------------------------- 1 | import type { PromptObject } from 'prompts'; 2 | 3 | export const PromptInit = [ 4 | { 5 | type: 'select', 6 | name: 'configFormat', 7 | message: 'What format do you want your config file to be in?', 8 | choices: [ 9 | { title: 'JSON', value: 'json' }, 10 | { title: 'YAML', value: 'yml' } 11 | ] 12 | }, 13 | { 14 | type: 'select', 15 | name: 'projectLanguage', 16 | message: 'Choose the language used in your project', 17 | choices: [ 18 | { title: 'TypeScript', value: 'ts' }, 19 | { title: 'JavaScript', value: 'js' } 20 | ] 21 | }, 22 | { 23 | type: 'text', 24 | name: 'base', 25 | message: 'Your base directory', 26 | initial: 'src' 27 | }, 28 | { 29 | type: 'text', 30 | name: 'commands', 31 | message: 'Where do you store your commands? (do not include the base)', 32 | initial: 'commands' 33 | }, 34 | { 35 | type: 'text', 36 | name: 'listeners', 37 | message: 'Where do you store your listeners? (do not include the base)', 38 | initial: 'listeners' 39 | }, 40 | { 41 | type: 'text', 42 | name: 'arguments', 43 | message: 'Where do you store your arguments? (do not include the base)', 44 | initial: 'arguments' 45 | }, 46 | { 47 | type: 'text', 48 | name: 'preconditions', 49 | message: 'Where do you store your preconditions? (do not include the base)', 50 | initial: 'preconditions' 51 | }, 52 | { 53 | type: 'text', 54 | name: 'interaction-handlers', 55 | message: 'Where do you store your interaction handlers? (do not include the base)', 56 | initial: 'interaction-handlers' 57 | }, 58 | { 59 | type: 'confirm', 60 | name: 'rEnabled', 61 | message: 'Would you use the api plugin?' 62 | }, 63 | { 64 | type: (prev) => (prev ? 'text' : null), 65 | name: 'rLocation', 66 | message: 'Where do you store your routes? (do not include the base)', 67 | initial: 'routes' 68 | }, 69 | { 70 | type: 'confirm', 71 | name: 'cftEnabled', 72 | message: 'Do you want to enable custom file templates?' 73 | }, 74 | { 75 | type: (prev) => (prev ? 'text' : null), 76 | name: 'cftLocation', 77 | message: 'Where do you store your custom file templates?', 78 | initial: 'templates' 79 | } 80 | ] as PromptObject[]; 81 | 82 | export type PromptInitObjectKeys = 83 | | 'configFormat' 84 | | 'projectLanguage' 85 | | 'base' 86 | | 'commands' 87 | | 'listeners' 88 | | 'arguments' 89 | | 'preconditions' 90 | | 'interaction-handlers' 91 | | 'routes' 92 | | 'cftEnabled' 93 | | 'cftLocation'; 94 | -------------------------------------------------------------------------------- /src/prompts/PromptNew.ts: -------------------------------------------------------------------------------- 1 | import type { Choice, PromptObject } from 'prompts'; 2 | 3 | const tsTemplates: Choice[] = [ 4 | { title: 'Starter template (Recommended)', value: 'with-typescript-starter' }, 5 | { title: 'Complete template', value: 'with-typescript-complete' }, 6 | { title: 'with tsup', value: 'with-tsup' }, 7 | { title: 'with SWC', value: 'with-swc' } 8 | ]; 9 | 10 | const jsTemplates: Choice[] = [ 11 | { title: 'with ESM (Recommended)', value: 'with-esm' }, 12 | { title: 'with CommonJS', value: 'with-javascript' } 13 | ]; 14 | 15 | export const PromptNew = (projectName: string, yarn: boolean, pnpm: boolean): PromptObject[] => { 16 | const pmChoices = [ 17 | { 18 | title: `Yarn (Recommended) ${yarn ? '' : '(Not installed)'}`, 19 | value: 'Yarn', 20 | disabled: !yarn 21 | }, 22 | { 23 | title: `pnpm ${pnpm ? '' : '(Not Installed)'}`, 24 | value: 'pnpm', 25 | disabled: !pnpm 26 | }, 27 | { title: 'npm', value: 'npm' } 28 | ]; 29 | 30 | return [ 31 | { 32 | type: 'text', 33 | name: 'projectName', 34 | message: "What's the name of your project?", 35 | initial: projectName ?? 'my-sapphire-bot' 36 | }, 37 | { 38 | type: 'select', 39 | name: 'projectLang', 40 | message: 'Choose a language for your project', 41 | choices: [ 42 | { title: 'TypeScript (Recommended)', value: 'ts' }, 43 | { title: 'JavaScript', value: 'js' } 44 | ] 45 | }, 46 | { 47 | type: 'select', 48 | name: 'projectTemplate', 49 | message: 'Choose a template for your project', 50 | choices: (prev: string) => (prev === 'ts' ? tsTemplates : jsTemplates) 51 | }, 52 | { 53 | type: 'select', 54 | name: 'configFormat', 55 | message: 'What format do you want your config file to be in?', 56 | choices: [ 57 | { title: 'JSON', value: 'json' }, 58 | { title: 'YAML', value: 'yml' } 59 | ] 60 | }, 61 | { 62 | type: 'select', 63 | name: 'packageManager', 64 | message: 'What package manager do you want to use?', 65 | choices: yarn ? pmChoices : pmChoices.slice().reverse() 66 | }, 67 | { 68 | type: (prev) => (prev === 'Yarn' ? 'confirm' : false), 69 | name: 'yarnV4', 70 | message: 'Do you want to use Yarn v4?', 71 | initial: true 72 | }, 73 | { 74 | type: 'confirm', 75 | name: 'git', 76 | message: 'Do you want to create a git repository for this project?' 77 | } 78 | ] as PromptObject[]; 79 | }; 80 | 81 | export type PromptNewObjectKeys = 'projectName' | 'projectLang' | 'projectTemplate' | 'packageManager' | 'configFormat' | 'git' | 'yarnV4'; 82 | -------------------------------------------------------------------------------- /src/functions/CreateFileFromTemplate.ts: -------------------------------------------------------------------------------- 1 | import { locationReplacement, templatesFolder } from '#constants'; 2 | import { fileExists } from '#functions/FileExists'; 3 | import type { Config } from '#lib/types'; 4 | import { mkdir, readFile, writeFile } from 'node:fs/promises'; 5 | import { dirname, join, resolve } from 'node:path'; 6 | 7 | export async function CreateFileFromTemplate( 8 | template: string, 9 | target: string, 10 | config: Config | null, 11 | params?: Record, 12 | custom = false, 13 | component = false 14 | ) { 15 | const location = custom ? template : join(templatesFolder, template); 16 | 17 | const output = {} as FileOutput; 18 | 19 | if (component) { 20 | const [config, templateContent] = await getComponentTemplateWithConfig(location); 21 | 22 | output.config = config; 23 | output.templateContent = templateContent; 24 | } 25 | 26 | output.templateContent ??= await readFile(location, 'utf8'); 27 | 28 | if (!output.templateContent) { 29 | throw new Error("Couldn't read the template file. Are you sure it exists, the name is correct, and the content is valid?"); 30 | } 31 | 32 | if (params) { 33 | for (const param of Object.entries(params)) { 34 | output.templateContent = output.templateContent.replaceAll(`{{${param[0]}}}`, param[1]); 35 | } 36 | } 37 | 38 | if (!output || (component && (!output.config || !output.config.category))) { 39 | throw new Error('The template is invalid. Please create a valid template structure.'); 40 | } 41 | 42 | const directoryForOutput = component ? config?.locations[output.config!.category] : null; 43 | const targetPath = component ? target.replace(locationReplacement, directoryForOutput) : target; 44 | 45 | if (await fileExists(targetPath)) { 46 | throw new Error('A component with the provided name already exists. Please provide a unique name.'); 47 | } 48 | 49 | await writeFileRecursive(targetPath, output.templateContent); 50 | 51 | return true; 52 | } 53 | 54 | /** 55 | * Gets the template and the config from a component template 56 | * @param path Path to the template 57 | * @returns [config, template] The config and the template 58 | */ 59 | async function getComponentTemplateWithConfig(path: string): Promise<[config: Record, template: string]> { 60 | const file = await readFile(path, 'utf8'); 61 | const fa = file.split(/---(\r\n|\r|\n|)/gm); 62 | return [JSON.parse(fa[0]), fa[2]]; 63 | } 64 | 65 | /** 66 | * Writes a file recursively 67 | * @param target Target path 68 | * @param data Data to write 69 | */ 70 | async function writeFileRecursive(target: string, data: string) { 71 | const resolvedTarget = resolve(target); 72 | const dir = dirname(resolvedTarget); 73 | 74 | await mkdir(dir, { recursive: true }); 75 | 76 | return writeFile(resolvedTarget, data); 77 | } 78 | 79 | interface FileOutput { 80 | templateContent: string; 81 | config?: Record; 82 | } 83 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | [changelog] 2 | header = """ 3 | # Changelog 4 | 5 | All notable changes to this project will be documented in this file.\n 6 | """ 7 | body = """ 8 | {%- macro remote_url() -%} 9 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} 10 | {%- endmacro -%} 11 | {% if version %}\ 12 | # [{{ version | trim_start_matches(pat="v") }}]\ 13 | {% if previous %}\ 14 | {% if previous.version %}\ 15 | ({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }})\ 16 | {% else %}\ 17 | ({{ self::remote_url() }}/tree/{{ version }})\ 18 | {% endif %}\ 19 | {% endif %} \ 20 | - ({{ timestamp | date(format="%Y-%m-%d") }}) 21 | {% else %}\ 22 | # [unreleased] 23 | {% endif %}\ 24 | {% for group, commits in commits | group_by(attribute="group") %} 25 | ## {{ group | upper_first }} 26 | {% for commit in commits %} 27 | - {% if commit.scope %}\ 28 | **{{commit.scope}}:** \ 29 | {% endif %}\ 30 | {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ 31 | {% if commit.github.pr_number %} (\ 32 | [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) by @{{ commit.github.username }}) \ 33 | {%- endif %}\ 34 | {% if commit.breaking %}\ 35 | {% for breakingChange in commit.footers %}\ 36 | \n{% raw %} {% endraw %}- 💥 **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ 37 | {% endfor %}\ 38 | {% endif %}\ 39 | {% endfor %} 40 | {% endfor %}\n 41 | """ 42 | trim = true 43 | footer = "" 44 | 45 | [git] 46 | conventional_commits = true 47 | filter_unconventional = true 48 | commit_parsers = [ 49 | { message = "^feat", group = "🚀 Features" }, 50 | { message = "^fix", group = "🐛 Bug Fixes" }, 51 | { message = "^docs", group = "📝 Documentation" }, 52 | { message = "^perf", group = "🏃 Performance" }, 53 | { message = "^refactor", group = "🏠 Refactor" }, 54 | { message = "^typings", group = "⌨️ Typings" }, 55 | { message = "^types", group = "⌨️ Typings" }, 56 | { message = ".*deprecated", body = ".*deprecated", group = "🚨 Deprecation" }, 57 | { message = "^revert", skip = true }, 58 | { message = "^style", group = "🪞 Styling" }, 59 | { message = "^test", group = "🧪 Testing" }, 60 | { message = "^chore", skip = true }, 61 | { message = "^ci", skip = true }, 62 | { message = "^build", skip = true }, 63 | { body = ".*security", group = "🛡️ Security" }, 64 | ] 65 | commit_preprocessors = [ 66 | # remove issue numbers from commits 67 | { pattern = '\s\((\w+\s)?#([0-9]+)\)', replace = "" }, 68 | ] 69 | filter_commits = true 70 | tag_pattern = "v[0-9]*" 71 | ignore_tags = "" 72 | topo_order = false 73 | sort_commits = "newest" 74 | 75 | [remote.github] 76 | owner = "sapphiredev" 77 | repo = "cli" 78 | -------------------------------------------------------------------------------- /src/functions/CommandExists.ts: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2019 Raphaël Thériault 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | import { fileExists } from '#functions/FileExists'; 26 | import { Result } from '@sapphire/result'; 27 | import { execa, type Result as ExecaReturnValue } from 'execa'; 28 | import { constants } from 'node:fs'; 29 | import { access } from 'node:fs/promises'; 30 | 31 | const windows = process.platform === 'win32'; 32 | 33 | async function isExecutable(command: string): Promise { 34 | const result = await Result.fromAsync(() => access(command, constants.X_OK)); 35 | 36 | return result.isErr(); 37 | } 38 | 39 | function cleanWindowsCommand(input: string) { 40 | if (/[^A-Za-z0-9_\/:=-]/.test(input)) { 41 | input = `'${input.replace(/'/g, "'\\''")}'`; 42 | input = input.replace(/^(?:'')+/g, '').replace(/\\'''/g, "\\'"); 43 | } 44 | 45 | return input; 46 | } 47 | 48 | async function commandExistsUnix(command: string): Promise { 49 | if (await fileExists(command)) { 50 | if (await isExecutable(command)) { 51 | return true; 52 | } 53 | } 54 | 55 | const result = await Result.fromAsync(() => execa('which', [command])); 56 | return result.match({ 57 | err: () => false, 58 | ok: (value: ExecaReturnValue) => Boolean(value.stdout) 59 | }); 60 | } 61 | 62 | const invalidWindowsCommandNameRegex = /[\x00-\x1f<>:"|?*]/; 63 | 64 | async function commandExistsWindows(command: string): Promise { 65 | if (invalidWindowsCommandNameRegex.test(command)) { 66 | return false; 67 | } 68 | 69 | const result = await Result.fromAsync(async () => execa('where', [cleanWindowsCommand(command)])); 70 | return result.match({ 71 | err: () => fileExists(command), 72 | ok: () => true 73 | }); 74 | } 75 | 76 | export async function CommandExists(command: string): Promise { 77 | return windows ? commandExistsWindows(command) : commandExistsUnix(command); 78 | } 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sapphire/cli", 3 | "version": "1.9.3", 4 | "description": "CLI for Sapphire Framework", 5 | "author": "@sapphire", 6 | "license": "MIT", 7 | "type": "module", 8 | "main": "dist/cli.js", 9 | "bin": { 10 | "sapphire": "./dist/cli.js" 11 | }, 12 | "imports": { 13 | "#prompts/*": "./dist/prompts/*.js", 14 | "#lib/*": "./dist/lib/*.js", 15 | "#functions/*": "./dist/functions/*.js", 16 | "#commands/*": "./dist/commands/*.js", 17 | "#constants": "./dist/constants.js" 18 | }, 19 | "sideEffects": "false", 20 | "files": [ 21 | "dist/**/*.js", 22 | "templates" 23 | ], 24 | "scripts": { 25 | "lint": "eslint src --ext ts --fix", 26 | "prettier": "prettier --ignore-path=.prettierignore", 27 | "format": "prettier --write .", 28 | "build": "tsc -b src", 29 | "clean": "tsc -b src --clean", 30 | "watch": "tsc -b src -w", 31 | "bump": "cliff-jumper", 32 | "check-update": "cliff-jumper --dry-run", 33 | "prepack": "yarn build" 34 | }, 35 | "dependencies": { 36 | "@favware/colorette-spinner": "^1.0.1", 37 | "@sapphire/node-utilities": "^1.0.2", 38 | "@sapphire/result": "^2.8.0", 39 | "colorette": "^2.0.20", 40 | "commander": "^14.0.2", 41 | "execa": "^9.6.1", 42 | "find-up": "^5.0.0", 43 | "js-yaml": "^4.1.1", 44 | "prompts": "^2.4.2", 45 | "tslib": "^2.8.1" 46 | }, 47 | "devDependencies": { 48 | "@commitlint/cli": "^20.2.0", 49 | "@commitlint/config-conventional": "^20.2.0", 50 | "@favware/cliff-jumper": "^6.0.0", 51 | "@favware/npm-deprecate": "^2.0.0", 52 | "@sapphire/decorators": "*", 53 | "@sapphire/eslint-config": "^5.0.6", 54 | "@sapphire/framework": "*", 55 | "@sapphire/plugin-api": "*", 56 | "@sapphire/prettier-config": "^2.0.0", 57 | "@sapphire/ts-config": "^5.0.3", 58 | "@types/js-yaml": "^4.0.9", 59 | "@types/node": "^24.10.4", 60 | "@types/prompts": "^2.4.9", 61 | "@typescript-eslint/eslint-plugin": "^7.13.0", 62 | "@typescript-eslint/parser": "^7.13.0", 63 | "cz-conventional-changelog": "^3.3.0", 64 | "discord.js": "*", 65 | "eslint": "^8.57.1", 66 | "eslint-config-prettier": "^10.1.8", 67 | "eslint-plugin-prettier": "^5.5.4", 68 | "globby": "^16.0.0", 69 | "lint-staged": "^16.2.7", 70 | "prettier": "^3.7.4", 71 | "ts-node": "^10.9.2", 72 | "typescript": "~5.4.5" 73 | }, 74 | "resolutions": { 75 | "ansi-regex": "^5.0.1", 76 | "minimist": "^1.2.8" 77 | }, 78 | "engines": { 79 | "node": ">=v18" 80 | }, 81 | "keywords": [ 82 | "@sapphire/cli", 83 | "bot", 84 | "typescript", 85 | "ts", 86 | "yarn", 87 | "discord", 88 | "sapphire", 89 | "discordjs" 90 | ], 91 | "repository": { 92 | "type": "git", 93 | "url": "git+https://github.com/sapphiredev/cli.git" 94 | }, 95 | "bugs": { 96 | "url": "https://github.com/sapphiredev/cli/issues" 97 | }, 98 | "homepage": "https://www.sapphirejs.dev", 99 | "commitlint": { 100 | "extends": [ 101 | "@commitlint/config-conventional" 102 | ] 103 | }, 104 | "lint-staged": { 105 | "*": "prettier --ignore-unknown --write", 106 | "*.{mjs,js,ts}": "eslint --fix --ext mjs,js,ts" 107 | }, 108 | "config": { 109 | "commitizen": { 110 | "path": "./node_modules/cz-conventional-changelog" 111 | } 112 | }, 113 | "publishConfig": { 114 | "access": "public" 115 | }, 116 | "prettier": "@sapphire/prettier-config", 117 | "packageManager": "yarn@4.12.0" 118 | } 119 | -------------------------------------------------------------------------------- /src/commands/generate.ts: -------------------------------------------------------------------------------- 1 | import { componentsFolder, locationReplacement } from '#constants'; 2 | import { CreateFileFromTemplate } from '#functions/CreateFileFromTemplate'; 3 | import { fileExists } from '#functions/FileExists'; 4 | import { generateCommandFlow } from '#functions/generateCommandFlow'; 5 | import { commandNames, componentCommandNames, componentInteractionHandlerNames, interactionHandlerNames } from '#lib/aliases'; 6 | import type { Config } from '#lib/types'; 7 | import { basename, join } from 'node:path'; 8 | 9 | /** 10 | * Generates a component based on the given component type and name. 11 | * @param component The type of component to generate. 12 | * @param name The name of the component. 13 | * @returns A Promise that resolves when the component generation is complete. 14 | */ 15 | export default async (component: string, name: string): Promise => { 16 | return generateCommandFlow('Creating loaders...', (config, configLocation) => createComponent(component, name, config, configLocation)); 17 | }; 18 | 19 | /** 20 | * Joins an array of component names into a single string. 21 | * 22 | * @param components - An array of component names. 23 | * @returns The joined string with component names. 24 | */ 25 | function joinComponentNames(components: string[]): string { 26 | const lastComponent = components.pop(); 27 | return `"${components.join('", "')}" or "${lastComponent}"`; 28 | } 29 | 30 | /** 31 | * Creates a component based on the specified parameters. 32 | * 33 | * @param component - The type of component to create. 34 | * @param name - The name of the component. 35 | * @param config - The configuration object. 36 | * @param configLoc - The location of the configuration file. 37 | * @returns A Promise that resolves when the component is created. 38 | * @throws An error if the 'projectLanguage' field is missing in the configuration file, 39 | * or if a template file for the component type cannot be found. 40 | */ 41 | async function createComponent(component: string, name: string, config: Config, configLoc: string) { 42 | const { projectLanguage } = config; 43 | 44 | if (!projectLanguage) { 45 | throw new Error("There is no 'projectLanguage' field in .sapphirerc.json"); 46 | } 47 | 48 | const template = `${component.toLowerCase()}.${projectLanguage}.sapphire`; 49 | 50 | const corePath = `${componentsFolder}${template}`; 51 | const userPath = config.customFileTemplates.enabled ? join(configLoc, config.customFileTemplates.location, template) : null; 52 | const target = join(configLoc, config.locations.base, locationReplacement, `${name}.${projectLanguage}`); 53 | const params = { name: basename(name) }; 54 | 55 | if (userPath && (await fileExists(userPath))) { 56 | return CreateFileFromTemplate(userPath, target, config, params, true, true); 57 | } else if (await fileExists(corePath)) { 58 | return CreateFileFromTemplate(`components/${template}`, target, config, params, false, true); 59 | } 60 | 61 | throw new Error(`Couldn't find a template file for that component type.${parseCommonHints(component)}`); 62 | } 63 | 64 | /** 65 | * Parses common hints for the user 66 | * @param component Component name 67 | * @returns A string with a hint for the user 68 | */ 69 | function parseCommonHints(component: string): string { 70 | const newLine = '\n'; 71 | const lowerCaseComponent = component.toLowerCase(); 72 | const commonHints = `${newLine}Hint: You wrote "${component}", instead of `; 73 | 74 | if (commandNames.includes(lowerCaseComponent)) { 75 | return `${commonHints}"${joinComponentNames(componentCommandNames)}".`; 76 | } 77 | 78 | if (interactionHandlerNames.includes(lowerCaseComponent)) { 79 | return `${commonHints} ${joinComponentNames(componentInteractionHandlerNames)}.`; 80 | } 81 | 82 | return ''; 83 | } 84 | -------------------------------------------------------------------------------- /src/functions/CreateComponentLoader.ts: -------------------------------------------------------------------------------- 1 | import { locationReplacement } from '#constants'; 2 | import type { Config } from '#lib/types'; 3 | import { findFilesRecursivelyRegex } from '@sapphire/node-utilities'; 4 | import { Result } from '@sapphire/result'; 5 | import { accessSync } from 'node:fs'; 6 | import { mkdir, writeFile } from 'node:fs/promises'; 7 | import { dirname, join, relative, resolve } from 'node:path'; 8 | 9 | /** 10 | * Regular expression pattern for matching files ending with JavaScript-like extensions. 11 | * - `.js` 12 | * - `.ts` 13 | * - `.mjs` 14 | * - `.cjs` 15 | * - `.mts` 16 | * - `.cts` 17 | */ 18 | const regexForFilesEndingWithJavaScriptLikeExtensions = /\.([mc])?[jt]s$/; 19 | 20 | /** 21 | * Regular expression used to match loader files. 22 | */ 23 | const regexForLoaderFiles = new RegExp(`_load${regexForFilesEndingWithJavaScriptLikeExtensions.source}`); 24 | 25 | /** 26 | * Generates loader file content 27 | * @param dir The directory to generate the loader for 28 | * @param targetDir The directory that the `_load.` file will be written to 29 | */ 30 | async function generateVirtualPieceLoader(dir: string, targetDir: string) { 31 | console.log(`Generating virtual piece loader for ${targetDir}`); 32 | 33 | const files: string[] = []; 34 | for await (const file of findFilesRecursivelyRegex(targetDir, regexForFilesEndingWithJavaScriptLikeExtensions)) { 35 | if (regexForLoaderFiles.test(file)) continue; 36 | files.push(relative(targetDir, file).replace(regexForFilesEndingWithJavaScriptLikeExtensions, '.$1js')); 37 | } 38 | 39 | console.log(`Found ${files.length} ${dir} files`); 40 | 41 | return `${files.map((file) => `import './${file}';`).join('\n')}\n`; 42 | } 43 | 44 | /** 45 | * Creates component loaders 46 | * 47 | * We wrap the bulk of this function in a {@link Result.fromAsync} so that if any of the file writing fails 48 | * that will bubble up as a failure overall. 49 | * 50 | * @param targetDir The directory that the `_load.` file will be written to 51 | * @param config The config 52 | * @returns Whether the loaders were created successfully 53 | */ 54 | export async function CreateComponentLoaders(targetDir: string, config: Config) { 55 | const templateHeader = `// import this file in your entry point (index.${config.projectLanguage}) to load respective pieces`; 56 | 57 | return ( 58 | await Result.fromAsync(async () => { 59 | const dirs = Object.entries(config.locations) 60 | .filter(([key]) => key !== 'base') 61 | .map(([, value]) => value) 62 | .filter((dir) => Result.from(() => accessSync(injectDirIntoTargetDir(dir, targetDir))).isOk()); 63 | 64 | for (const dir of dirs) { 65 | const dirInjectedTarget = injectDirIntoTargetDir(dir, targetDir); 66 | const target = join(dirInjectedTarget, `_load.${config.projectLanguage}`); 67 | 68 | const content = `${templateHeader}\n${await generateVirtualPieceLoader(dir, dirInjectedTarget)}`; 69 | await writeFileRecursive(target, content); 70 | } 71 | }) 72 | ).isOk(); 73 | } 74 | 75 | /** 76 | * Replaces the placeholder {@link locationReplacement} in the target directory with the specified directory. 77 | * 78 | * @param dir The directory to be injected into the target directory. 79 | * @param targetDir The target directory containing the placeholder {@link locationReplacement}. 80 | * @returns The target directory with the placeholder replaced by the specified directory. 81 | */ 82 | function injectDirIntoTargetDir(dir: string, targetDir: string) { 83 | return targetDir.replace(locationReplacement, dir); 84 | } 85 | 86 | /** 87 | * Writes data to a file recursively. 88 | * 89 | * @param target - The target file path. 90 | * @param data - The data to write to the file. 91 | * @returns A promise that resolves when the file is written. 92 | */ 93 | async function writeFileRecursive(target: string, data: string) { 94 | const resolvedTarget = resolve(target); 95 | const dir = dirname(resolvedTarget); 96 | 97 | await mkdir(dir, { recursive: true }); 98 | 99 | return writeFile(resolvedTarget, data); 100 | } 101 | -------------------------------------------------------------------------------- /.yarn/plugins/@yarnpkg/plugin-git-hooks.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | //prettier-ignore 3 | module.exports = { 4 | name: "@yarnpkg/plugin-git-hooks", 5 | factory: function (require) { 6 | var plugin=(()=>{var p=Object.create;var i=Object.defineProperty;var u=Object.getOwnPropertyDescriptor;var l=Object.getOwnPropertyNames;var P=Object.getPrototypeOf,m=Object.prototype.hasOwnProperty;var _=(n=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(n,{get:(e,E)=>(typeof require<"u"?require:e)[E]}):n)(function(n){if(typeof require<"u")return require.apply(this,arguments);throw new Error('Dynamic require of "'+n+'" is not supported')});var c=(n,e)=>()=>(e||n((e={exports:{}}).exports,e),e.exports),A=(n,e)=>{for(var E in e)i(n,E,{get:e[E],enumerable:!0})},C=(n,e,E,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let I of l(e))!m.call(n,I)&&I!==E&&i(n,I,{get:()=>e[I],enumerable:!(s=u(e,I))||s.enumerable});return n};var U=(n,e,E)=>(E=n!=null?p(P(n)):{},C(e||!n||!n.__esModule?i(E,"default",{value:n,enumerable:!0}):E,n)),v=n=>C(i({},"__esModule",{value:!0}),n);var L=c((M,B)=>{B.exports=[{name:"Appcircle",constant:"APPCIRCLE",env:"AC_APPCIRCLE"},{name:"AppVeyor",constant:"APPVEYOR",env:"APPVEYOR",pr:"APPVEYOR_PULL_REQUEST_NUMBER"},{name:"AWS CodeBuild",constant:"CODEBUILD",env:"CODEBUILD_BUILD_ARN"},{name:"Azure Pipelines",constant:"AZURE_PIPELINES",env:"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI",pr:"SYSTEM_PULLREQUEST_PULLREQUESTID"},{name:"Bamboo",constant:"BAMBOO",env:"bamboo_planKey"},{name:"Bitbucket Pipelines",constant:"BITBUCKET",env:"BITBUCKET_COMMIT",pr:"BITBUCKET_PR_ID"},{name:"Bitrise",constant:"BITRISE",env:"BITRISE_IO",pr:"BITRISE_PULL_REQUEST"},{name:"Buddy",constant:"BUDDY",env:"BUDDY_WORKSPACE_ID",pr:"BUDDY_EXECUTION_PULL_REQUEST_ID"},{name:"Buildkite",constant:"BUILDKITE",env:"BUILDKITE",pr:{env:"BUILDKITE_PULL_REQUEST",ne:"false"}},{name:"CircleCI",constant:"CIRCLE",env:"CIRCLECI",pr:"CIRCLE_PULL_REQUEST"},{name:"Cirrus CI",constant:"CIRRUS",env:"CIRRUS_CI",pr:"CIRRUS_PR"},{name:"Codefresh",constant:"CODEFRESH",env:"CF_BUILD_ID",pr:{any:["CF_PULL_REQUEST_NUMBER","CF_PULL_REQUEST_ID"]}},{name:"Codemagic",constant:"CODEMAGIC",env:"CM_BUILD_ID",pr:"CM_PULL_REQUEST"},{name:"Codeship",constant:"CODESHIP",env:{CI_NAME:"codeship"}},{name:"Drone",constant:"DRONE",env:"DRONE",pr:{DRONE_BUILD_EVENT:"pull_request"}},{name:"dsari",constant:"DSARI",env:"DSARI"},{name:"Expo Application Services",constant:"EAS",env:"EAS_BUILD"},{name:"Gerrit",constant:"GERRIT",env:"GERRIT_PROJECT"},{name:"GitHub Actions",constant:"GITHUB_ACTIONS",env:"GITHUB_ACTIONS",pr:{GITHUB_EVENT_NAME:"pull_request"}},{name:"GitLab CI",constant:"GITLAB",env:"GITLAB_CI",pr:"CI_MERGE_REQUEST_ID"},{name:"GoCD",constant:"GOCD",env:"GO_PIPELINE_LABEL"},{name:"Google Cloud Build",constant:"GOOGLE_CLOUD_BUILD",env:"BUILDER_OUTPUT"},{name:"Harness CI",constant:"HARNESS",env:"HARNESS_BUILD_ID"},{name:"Heroku",constant:"HEROKU",env:{env:"NODE",includes:"/app/.heroku/node/bin/node"}},{name:"Hudson",constant:"HUDSON",env:"HUDSON_URL"},{name:"Jenkins",constant:"JENKINS",env:["JENKINS_URL","BUILD_ID"],pr:{any:["ghprbPullId","CHANGE_ID"]}},{name:"LayerCI",constant:"LAYERCI",env:"LAYERCI",pr:"LAYERCI_PULL_REQUEST"},{name:"Magnum CI",constant:"MAGNUM",env:"MAGNUM"},{name:"Netlify CI",constant:"NETLIFY",env:"NETLIFY",pr:{env:"PULL_REQUEST",ne:"false"}},{name:"Nevercode",constant:"NEVERCODE",env:"NEVERCODE",pr:{env:"NEVERCODE_PULL_REQUEST",ne:"false"}},{name:"ReleaseHub",constant:"RELEASEHUB",env:"RELEASE_BUILD_ID"},{name:"Render",constant:"RENDER",env:"RENDER",pr:{IS_PULL_REQUEST:"true"}},{name:"Sail CI",constant:"SAIL",env:"SAILCI",pr:"SAIL_PULL_REQUEST_NUMBER"},{name:"Screwdriver",constant:"SCREWDRIVER",env:"SCREWDRIVER",pr:{env:"SD_PULL_REQUEST",ne:"false"}},{name:"Semaphore",constant:"SEMAPHORE",env:"SEMAPHORE",pr:"PULL_REQUEST_NUMBER"},{name:"Shippable",constant:"SHIPPABLE",env:"SHIPPABLE",pr:{IS_PULL_REQUEST:"true"}},{name:"Solano CI",constant:"SOLANO",env:"TDDIUM",pr:"TDDIUM_PR_ID"},{name:"Sourcehut",constant:"SOURCEHUT",env:{CI_NAME:"sourcehut"}},{name:"Strider CD",constant:"STRIDER",env:"STRIDER"},{name:"TaskCluster",constant:"TASKCLUSTER",env:["TASK_ID","RUN_ID"]},{name:"TeamCity",constant:"TEAMCITY",env:"TEAMCITY_VERSION"},{name:"Travis CI",constant:"TRAVIS",env:"TRAVIS",pr:{env:"TRAVIS_PULL_REQUEST",ne:"false"}},{name:"Vercel",constant:"VERCEL",env:{any:["NOW_BUILDER","VERCEL"]}},{name:"Visual Studio App Center",constant:"APPCENTER",env:"APPCENTER_BUILD_ID"},{name:"Woodpecker",constant:"WOODPECKER",env:{CI:"woodpecker"},pr:{CI_BUILD_EVENT:"pull_request"}},{name:"Xcode Cloud",constant:"XCODE_CLOUD",env:"CI_XCODE_PROJECT",pr:"CI_PULL_REQUEST_NUMBER"},{name:"Xcode Server",constant:"XCODE_SERVER",env:"XCS"}]});var T=c(a=>{"use strict";var D=L(),t=process.env;Object.defineProperty(a,"_vendors",{value:D.map(function(n){return n.constant})});a.name=null;a.isPR=null;D.forEach(function(n){let E=(Array.isArray(n.env)?n.env:[n.env]).every(function(s){return S(s)});if(a[n.constant]=E,!!E)switch(a.name=n.name,typeof n.pr){case"string":a.isPR=!!t[n.pr];break;case"object":"env"in n.pr?a.isPR=n.pr.env in t&&t[n.pr.env]!==n.pr.ne:"any"in n.pr?a.isPR=n.pr.any.some(function(s){return!!t[s]}):a.isPR=S(n.pr);break;default:a.isPR=null}});a.isCI=!!(t.CI!=="false"&&(t.BUILD_ID||t.BUILD_NUMBER||t.CI||t.CI_APP_ID||t.CI_BUILD_ID||t.CI_BUILD_NUMBER||t.CI_NAME||t.CONTINUOUS_INTEGRATION||t.RUN_ID||a.name||!1));function S(n){return typeof n=="string"?!!t[n]:"env"in n?t[n.env]&&t[n.env].includes(n.includes):"any"in n?n.any.some(function(e){return!!t[e]}):Object.keys(n).every(function(e){return t[e]===n[e]})}});var d={};A(d,{default:()=>O});var o=U(_("process")),r=_("@yarnpkg/core"),R=U(T()),N={configuration:{gitHooksPath:{description:"Path to git hooks directory (recommended: .github/hooks)",type:r.SettingsType.STRING,default:null},disableGitHooks:{description:"Disable automatic git hooks installation",type:r.SettingsType.BOOLEAN,default:R.default.isCI}},hooks:{afterAllInstalled:async n=>{let e=n.configuration.get("gitHooksPath"),E=n.configuration.get("disableGitHooks"),s=Boolean(n.cwd?.endsWith(`dlx-${o.default.pid}`));if(e&&!R.default.isCI&&!s&&!E)return r.execUtils.pipevp("git",["config","core.hooksPath",e],{cwd:n.cwd,strict:!0,stdin:o.default.stdin,stdout:o.default.stdout,stderr:o.default.stderr})}}},O=N;return v(d);})(); 7 | return plugin; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/commands/new.ts: -------------------------------------------------------------------------------- 1 | import { repoUrl } from '#constants'; 2 | import { CommandExists } from '#functions/CommandExists'; 3 | import { CreateFileFromTemplate } from '#functions/CreateFileFromTemplate'; 4 | import { fileExists } from '#functions/FileExists'; 5 | import { PromptNew, type PromptNewObjectKeys } from '#prompts/PromptNew'; 6 | import { Spinner } from '@favware/colorette-spinner'; 7 | import { Result } from '@sapphire/result'; 8 | import { blueBright, red } from 'colorette'; 9 | import { execa } from 'execa'; 10 | import { cp, readFile, rm, writeFile } from 'node:fs/promises'; 11 | import { resolve } from 'node:path'; 12 | import prompts from 'prompts'; 13 | 14 | /** 15 | * Creates a new project with the given name and flags. 16 | * @param name - The name of the project. 17 | * @param flags - The flags for the project. 18 | * @returns A promise that resolves when the project setup is complete. 19 | */ 20 | export default async (name: string, flags: Record) => { 21 | const response = await prompts(PromptNew(name, await CommandExists('yarn'), await CommandExists('pnpm'))); 22 | 23 | if (!response.projectName || !response.projectLang || !response.projectTemplate || !response.packageManager) { 24 | process.exit(1); 25 | } 26 | 27 | const projectName = response.projectName === '.' ? '' : response.projectName; 28 | 29 | const stpJob = async () => { 30 | await cp(`./${response.projectName}/ghr/examples/${response.projectTemplate}/.`, `./${response.projectName}/`, { recursive: true }); 31 | 32 | for (const p of ['.gitignore', '.prettierignore']) { 33 | await cp(`./${response.projectName}/ghr/${p}`, `./${response.projectName}/${p}`, { recursive: true }); 34 | } 35 | 36 | await rm(`./${response.projectName}/ghr`, { recursive: true, force: true }); 37 | 38 | await CreateFileFromTemplate( 39 | `.sapphirerc.${response.configFormat}.sapphire`, 40 | resolve(`./${response.projectName}/.sapphirerc.${response.configFormat}`), 41 | null, 42 | { 43 | language: response.projectLang 44 | } 45 | ); 46 | 47 | await editPackageJson(response.projectName, projectName); 48 | 49 | if (response.packageManager === 'pnpm') { 50 | await writeFile(`./${response.projectName}/.npmrc`, '# pnpm only\nshamefully-hoist=true\npublic-hoist-pattern[]=@sapphire/*'); 51 | } 52 | }; 53 | 54 | const jobs: [() => any, string][] = [ 55 | [() => cloneRepo(response.projectName, flags.verbose), 'Cloning the repository'], 56 | [stpJob, 'Setting up the project'] 57 | ]; 58 | 59 | if (response.git) { 60 | jobs.push([() => initializeGitRepo(response.projectName), 'Initializing git repo']); 61 | } 62 | 63 | if (response.yarnV4) { 64 | jobs.push([() => installYarnV4(response.projectName, flags.verbose), 'Installing Yarn v4']); 65 | jobs.push([() => configureYarnRc(response.projectName, 'enableGlobalCache', 'true'), 'Enabling Yarn v4 global cache']); 66 | jobs.push([() => configureYarnRc(response.projectName, 'nodeLinker', 'node-modules'), 'Configuring Yarn v4 to use node-modules']); 67 | } 68 | 69 | jobs.push([ 70 | () => installDeps(response.projectName, response.packageManager, flags.verbose), 71 | `Installing dependencies using ${response.packageManager}` 72 | ]); 73 | 74 | for (const [job, name] of jobs) { 75 | await runJob(job, name).catch(() => process.exit(1)); 76 | } 77 | 78 | console.log(blueBright('Done!')); 79 | process.exit(0); 80 | }; 81 | 82 | /** 83 | * Edits the package.json file at the specified location by updating the "name" field. 84 | * @param location - The location of the package.json file. 85 | * @param name - The new value for the "name" field. 86 | * @returns A boolean indicating whether the operation was successful. 87 | */ 88 | async function editPackageJson(location: string, name: string) { 89 | const pjLocation = `./${location}/package.json`; 90 | const output = JSON.parse(await readFile(pjLocation, 'utf8')); 91 | if (!output) throw new Error("Can't read file."); 92 | 93 | output.name = name; 94 | 95 | const result = await Result.fromAsync(() => writeFile(pjLocation, JSON.stringify(output, null, 2))); 96 | 97 | return result.isOk(); 98 | } 99 | 100 | /** 101 | * Installs dependencies using the specified package manager. 102 | * @param location The location where the dependencies should be installed. 103 | * @param pm The package manager to use ('npm', 'Yarn', or 'pnpm'). 104 | * @param verbose Whether to display the installation output. 105 | * @returns A boolean indicating whether the installation was successful. 106 | */ 107 | async function installDeps(location: string, pm: 'npm' | 'Yarn' | 'pnpm', verbose: boolean) { 108 | const value = await execa(pm.toLowerCase(), ['install'], { 109 | stdio: verbose ? 'inherit' : undefined, 110 | cwd: `./${location}/` 111 | }); 112 | 113 | if (value.exitCode !== 0) { 114 | throw new Error('An unknown error occurred while installing the dependencies. Try running Sapphire CLI with "--verbose" flag.'); 115 | } 116 | 117 | const oppositeLockfiles = { 118 | npm: ['yarn.lock', 'pnpm-lock.yaml'], 119 | yarn: ['package-lock.json', 'pnpm-lock.yaml'], 120 | pnpm: ['package-lock.json', 'yarn.lock'] 121 | } as const; 122 | 123 | const lockfiles = pm === 'npm' ? oppositeLockfiles.npm : pm === 'Yarn' ? oppositeLockfiles.yarn : oppositeLockfiles.pnpm; 124 | 125 | for (const lockfile of lockfiles) { 126 | if (await fileExists(lockfile)) { 127 | await rm(lockfile); 128 | } 129 | } 130 | 131 | return true; 132 | } 133 | 134 | /** 135 | * Configures the yarnrc file with the specified name and value. 136 | * 137 | * @param location - The location of the yarnrc file. 138 | * @param name - The name of the configuration to set. 139 | * @param value - The value to set for the configuration. 140 | * @returns A promise that resolves to true if the configuration was successfully set, otherwise false. 141 | */ 142 | async function configureYarnRc(location: string, name: string, value: string) { 143 | await execa('yarn', ['config', 'set', name, value], { cwd: `./${location}/` }); 144 | return true; 145 | } 146 | 147 | /** 148 | * Installs Yarn v4 at the specified location. 149 | * 150 | * @param location - The location where Yarn v4 will be installed. 151 | * @param verbose - Whether to display verbose output during installation. 152 | * @returns A boolean indicating whether the installation was successful. 153 | * @throws An error if an unknown error occurs during installation. 154 | */ 155 | async function installYarnV4(location: string, verbose: boolean) { 156 | const valueSetVersion = await execa('yarn', ['set', 'version', 'berry'], { 157 | stdio: verbose ? 'inherit' : undefined, 158 | cwd: `./${location}/` 159 | }); 160 | 161 | if (valueSetVersion.exitCode !== 0) { 162 | throw new Error('An unknown error occurred while installing Yarn v4. Try running Sapphire CLI with "--verbose" flag.'); 163 | } 164 | 165 | return true; 166 | } 167 | 168 | /** 169 | * Initializes a Git repository at the specified location. 170 | * @param location - The location where the Git repository should be initialized. 171 | * @returns A boolean indicating whether the Git repository was successfully initialized. 172 | */ 173 | async function initializeGitRepo(location: string) { 174 | await execa('git', ['init'], { cwd: `./${location}/` }); 175 | return true; 176 | } 177 | 178 | /** 179 | * Runs a job asynchronously and handles the result. 180 | * 181 | * @param job - The job to be executed. 182 | * @param name - The name of the job. 183 | * @returns A boolean indicating whether the job was successful or not. 184 | */ 185 | async function runJob(job: () => Promise, name: string) { 186 | const spinner = new Spinner(name).start(); 187 | 188 | const result = await Result.fromAsync(() => job()); 189 | return result.match({ 190 | ok: () => { 191 | spinner.success(); 192 | return true; 193 | }, 194 | err: (error) => { 195 | spinner.error({ text: red(error.message) }); 196 | console.error(red(error.message)); 197 | throw error; 198 | } 199 | }); 200 | } 201 | 202 | /** 203 | * Clones a repository to the specified location. 204 | * @param location - The location where the repository will be cloned. 205 | * @param verbose - Whether to display the output of the cloning process. 206 | * @returns A boolean indicating whether the cloning was successful. 207 | * @throws An error if an unknown error occurred while cloning the repository. 208 | */ 209 | async function cloneRepo(location: string, verbose: boolean) { 210 | const value = await execa('git', ['clone', repoUrl, `${location}/ghr`], { stdio: verbose ? 'inherit' : undefined }); 211 | if (value.exitCode !== 0) { 212 | throw new Error('An unknown error occurred while cloning the repository. Try running Sapphire CLI with "--verbose" flag.'); 213 | } 214 | 215 | return true; 216 | } 217 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | # [1.9.3](https://github.com/sapphiredev/cli/compare/v1.9.3...v1.9.3) - (2024-01-27) 6 | 7 | ## 🐛 Bug Fixes 8 | 9 | - Fixed generate-loader generating invalid paths ([90c6b2d](https://github.com/sapphiredev/cli/commit/90c6b2d02c642b9d7aa8d3c3ed296f8c65df898e)) 10 | 11 | # [1.9.2](https://github.com/sapphiredev/cli/compare/v1.9.2...v1.9.2) - (2024-01-19) 12 | 13 | ## 🐛 Bug Fixes 14 | 15 | - Update transitive sapphire dependencies ([4f031ac](https://github.com/sapphiredev/cli/commit/4f031ac8e753ead46fbafbdc11be77d0fb9b5501)) 16 | 17 | # [1.9.1](https://github.com/sapphiredev/cli/compare/v1.9.1...v1.9.1) - (2023-12-04) 18 | 19 | ## 🐛 Bug Fixes 20 | 21 | - Update transitive dependencies ([5fc2d29](https://github.com/sapphiredev/cli/commit/5fc2d297229caa6396aafcc796d5bf50a92241e8)) 22 | 23 | # [1.9.0](https://github.com/sapphiredev/cli/compare/v1.9.0...v1.9.0) - (2023-12-03) 24 | 25 | ## 🐛 Bug Fixes 26 | 27 | - Specify schema for both template schema files ([30a80bb](https://github.com/sapphiredev/cli/commit/30a80bb561f61b8fba78c8cfc4157ca579e73f7c)) 28 | - **json schema:** Specify enum for projectLanguage ([2f1ba0d](https://github.com/sapphiredev/cli/commit/2f1ba0d7fb4597c82581601ee60391898a3b100f)) 29 | - **json schema:** Fixed base schema url ([bd37a6c](https://github.com/sapphiredev/cli/commit/bd37a6c5b590de90c2a1efa7cb19f16f38403273)) 30 | - Install yarn v4, not v3 ([938ca99](https://github.com/sapphiredev/cli/commit/938ca99fe6dedea04978ca1ac38ac9a4505f0651)) 31 | 32 | ## 📝 Documentation 33 | 34 | - **readme:** Update readme to mention virtual components ([23aa8c5](https://github.com/sapphiredev/cli/commit/23aa8c5e6b0f07e4b8028c21ecb243e81d845e03)) 35 | 36 | ## 🚀 Features 37 | 38 | - Add generator for virtual piece loader (#274) ([f9fa2a3](https://github.com/sapphiredev/cli/commit/f9fa2a359c9a1bdb3b512a7484e0da12e20f6c8b)) 39 | 40 | # [1.8.0](https://github.com/sapphiredev/cli/compare/v1.8.0...v1.8.0) - (2023-11-16) 41 | 42 | ## 🚀 Features 43 | 44 | - Bump minimum nodejs version to >= 18 ([3343626](https://github.com/sapphiredev/cli/commit/334362647a45bc99e5763028d5b848ce514cdda2)) 45 | 46 | # [1.7.0](https://github.com/sapphiredev/cli/compare/v1.7.0...v1.7.0) - (2023-11-16) 47 | 48 | ## 🐛 Bug Fixes 49 | 50 | - Update default JS templates to use new `LoaderContext` in doc comments ([47d5911](https://github.com/sapphiredev/cli/commit/47d5911bc0114161b951561ee6aa86043587804c)) 51 | - **deps:** Update dependency execa to v8 (#254) ([88880b2](https://github.com/sapphiredev/cli/commit/88880b29595236ef552573c54a718a6e262f820b)) 52 | - **deps:** Update dependency commander to v11 (#235) ([76d0719](https://github.com/sapphiredev/cli/commit/76d0719ed55e2e7ae43aba5c21c4843bd7660af7)) 53 | - **deps:** Update all non-major dependencies ([d647d3f](https://github.com/sapphiredev/cli/commit/d647d3f9fbefa118348543bba2c098ed84604071)) 54 | 55 | ## 🚀 Features 56 | 57 | - Bump minimum nodejs version to 16.17.0 ([28a4cc8](https://github.com/sapphiredev/cli/commit/28a4cc806aeb993a924ee52fc448655a6db157a0)) 58 | 59 | # [1.6.1](https://github.com/sapphiredev/cli/compare/v1.6.0...v1.6.1) - (2023-05-02) 60 | 61 | ## 🏃 Performance 62 | 63 | - Optimize the logic of "parseCommonHints" (#221) ([13260f0](https://github.com/sapphiredev/cli/commit/13260f02456f13e87c9257b97dcd03199e6bf57d)) 64 | 65 | ## 🐛 Bug Fixes 66 | 67 | - Output proper esm code ([353a861](https://github.com/sapphiredev/cli/commit/353a861455caed0a5762f6a6d9661bc5419000dc)) 68 | - Use proper imports ([7c7b54b](https://github.com/sapphiredev/cli/commit/7c7b54b700faf3f21a8cab16d2ce493ab2a8ff31)) 69 | - Ensure yarn v3 is configured properly ([f53d59a](https://github.com/sapphiredev/cli/commit/f53d59a77a59c8c2f2639bd5c0ad92759ff42d10)) 70 | - **deps:** Update all non-major dependencies ([60e7410](https://github.com/sapphiredev/cli/commit/60e7410eba394745477f8707c705c9444f02dff4)) 71 | 72 | # [1.6.0](https://github.com/sapphiredev/cli/compare/v1.5.0...v1.6.0) - (2023-04-12) 73 | 74 | ## 🏠 Refactor 75 | 76 | - Remove `with-docker` template ([1b485f8](https://github.com/sapphiredev/cli/commit/1b485f836d656022dd761cc13862fa9a970142a7)) 77 | - Stricter typing for config parsing ([ab0efb3](https://github.com/sapphiredev/cli/commit/ab0efb3c602843b76bf64e135c0a448288227462)) 78 | - **new:** Make yarn v3 the default ([30499ea](https://github.com/sapphiredev/cli/commit/30499ea72a52c31220794f41da695494145c9b08)) 79 | - **CreateFileFromTemplate:** Better internal code naming ([3189772](https://github.com/sapphiredev/cli/commit/3189772a1b85ecc2bad2949ed8d339ff0541cebf)) 80 | - Better error messages for `generate` when template doesn't exist ([dc34e1e](https://github.com/sapphiredev/cli/commit/dc34e1ecda585eeb7024d716fbf401e8e18a9b93)) 81 | 82 | ## 🐛 Bug Fixes 83 | 84 | - Better error messages when creating file ([afa4afb](https://github.com/sapphiredev/cli/commit/afa4afba579f4877eecbe447a9728b71b4042b0c)) 85 | - Fixed JSON config file ([6fe3d0c](https://github.com/sapphiredev/cli/commit/6fe3d0c87a0ba70a802d73c73028832b77c0077b)) 86 | - **templates:** Adhere to strict type checking rules ([b455738](https://github.com/sapphiredev/cli/commit/b455738705d475d99357f758d9eeea505d43c2f2)) 87 | 88 | ## 🚀 Features 89 | 90 | - Add route in prompt (#220) ([60451d6](https://github.com/sapphiredev/cli/commit/60451d6e2c92ef42c07f592d2923177aa9386595)) 91 | - **templates:** Add interaction handler templates (#216) ([650ec76](https://github.com/sapphiredev/cli/commit/650ec76c3c17e2ae5d480994daac4b42bacbfc34)) 92 | - Add `interactive-tools` plugin for yarn v3 installs ([c417d97](https://github.com/sapphiredev/cli/commit/c417d970f139da1827fe914f69903f90df436907)) 93 | 94 | ## 🪞 Styling 95 | 96 | - Add prettierignore file ([2d24595](https://github.com/sapphiredev/cli/commit/2d24595e347a9e4d24ca6926e35fb60945e11725)) 97 | 98 | # [1.5.0](https://github.com/sapphiredev/cli/compare/v1.4.0...v1.5.0) - (2023-04-10) 99 | 100 | ## 🐛 Bug Fixes 101 | 102 | - **deps:** Update dependency execa to v7 (#203) ([dd78817](https://github.com/sapphiredev/cli/commit/dd78817e287246ae17c5e0ca947a04adf49fb86c)) 103 | 104 | ## 🚀 Features 105 | 106 | - **templates:** Add route component (#215) ([2945f09](https://github.com/sapphiredev/cli/commit/2945f09a663421362ea3f34f28900fd193acf5d7)) 107 | 108 | # [1.4.0](https://github.com/sapphiredev/cli/compare/v1.3.1...v1.4.0) - (2023-01-29) 109 | 110 | ## 🏠 Refactor 111 | 112 | - Import discord types from discord.js ([542b63e](https://github.com/sapphiredev/cli/commit/542b63e1070f70cddc31e9c1b349c47f2de2f438)) 113 | - Update template to v14/djs and sapphire/v4 (#181) ([7c2d28b](https://github.com/sapphiredev/cli/commit/7c2d28b4b3fbae511c2e68aa237983f373cde032)) 114 | 115 | ## 🐛 Bug Fixes 116 | 117 | - **deps:** Update dependency commander to v10 (#196) ([8646ff0](https://github.com/sapphiredev/cli/commit/8646ff0187db16d5da2f7aae0a30ee1d9164e01a)) 118 | - **deps:** Update dependency @sapphire/result to ^2.6.0 (#176) ([309768b](https://github.com/sapphiredev/cli/commit/309768bdfebb22001c14f84cd46a1f750ae1afbd)) 119 | - **deps:** Update dependency @sapphire/result to ^2.5.0 ([c501908](https://github.com/sapphiredev/cli/commit/c50190879f6ffa1708a972c717cb3dd84eca0248)) 120 | 121 | ## 📝 Documentation 122 | 123 | - Add @BashGuy10 as a contributor ([2343600](https://github.com/sapphiredev/cli/commit/234360035defce7cb150e836ac7b8e2cf3d64dee)) 124 | 125 | ## 🚀 Features 126 | 127 | - Add typescript starter example ([2dedf93](https://github.com/sapphiredev/cli/commit/2dedf93f3a82b2853a8f8f142de57e0d25caf3f0)) 128 | - Add pnpm support (#191) ([1eec4e6](https://github.com/sapphiredev/cli/commit/1eec4e6e2c59676bb9ca46a4de2f23c6a726e1bb)) 129 | 130 | # [1.3.1](https://github.com/sapphiredev/cli/compare/v1.3.0...v1.3.1) - (2022-09-06) 131 | 132 | ## 🐛 Bug Fixes 133 | 134 | - Fixed core templates (#159) ([85ea98b](https://github.com/sapphiredev/cli/commit/85ea98babb5197e041367f3d47e12c18f753e4ea)) 135 | - **deps:** Update dependency @sapphire/result to ^2.4.1 ([1960263](https://github.com/sapphiredev/cli/commit/1960263268e38dfc8ccfb91a8b3621d4d0c3bf76)) 136 | - Update messagecommand to v3 and use Command (#152) ([f897118](https://github.com/sapphiredev/cli/commit/f897118b7edd9129068dea71dca865dc5c7b39ab)) 137 | 138 | # [1.3.0](https://github.com/sapphiredev/cli/compare/v1.2.0...v1.3.0) - (2022-08-21) 139 | 140 | ## 🏠 Refactor 141 | 142 | - Switch to @favware/colorette-spinner ([e52962d](https://github.com/sapphiredev/cli/commit/e52962d53bc11af482c4ba60186f411d94f29b0b)) 143 | 144 | ## 🐛 Bug Fixes 145 | 146 | - **deps:** Update dependency @sapphire/result to ^2.3.3 ([4c6891b](https://github.com/sapphiredev/cli/commit/4c6891b28134120969975b00ab474683eaa9cedd)) 147 | - **deps:** Update dependency @sapphire/result to ^2.1.1 ([e42c188](https://github.com/sapphiredev/cli/commit/e42c188b05ec2f4a68403f94cfa6333e9d5421fd)) 148 | - **deps:** Update dependency @sapphire/result to v2 (#135) ([025c7ca](https://github.com/sapphiredev/cli/commit/025c7caed86e17e4b9e20def743e7c33d9b81589)) 149 | 150 | ## 📝 Documentation 151 | 152 | - Add @boingtheboeing as a contributor ([ac6088c](https://github.com/sapphiredev/cli/commit/ac6088c557800b25f3da1bd561de3941298e5f22)) 153 | 154 | ## 🚀 Features 155 | 156 | - Add templates for slash commands and context menu commands (#141) ([b97aeac](https://github.com/sapphiredev/cli/commit/b97aeac80999e81a3ae80e0dc7c749d6474945a8)) 157 | 158 | # [1.3.1](https://github.com/sapphiredev/cli/compare/v1.3.0...v1.3.1) - (2022-09-06) 159 | 160 | ## 🐛 Bug Fixes 161 | 162 | - Fixed core templates (#159) ([85ea98b](https://github.com/sapphiredev/cli/commit/85ea98babb5197e041367f3d47e12c18f753e4ea)) 163 | - **deps:** Update dependency @sapphire/result to ^2.4.1 ([1960263](https://github.com/sapphiredev/cli/commit/1960263268e38dfc8ccfb91a8b3621d4d0c3bf76)) 164 | - Update messagecommand to v3 and use Command (#152) ([f897118](https://github.com/sapphiredev/cli/commit/f897118b7edd9129068dea71dca865dc5c7b39ab)) 165 | 166 | # [1.3.0](https://github.com/sapphiredev/cli/compare/v1.2.0...v1.3.0) - (2022-08-21) 167 | 168 | ## 🏠 Refactor 169 | 170 | - Switch to @favware/colorette-spinner ([e52962d](https://github.com/sapphiredev/cli/commit/e52962d53bc11af482c4ba60186f411d94f29b0b)) 171 | 172 | ## 🐛 Bug Fixes 173 | 174 | - **deps:** Update dependency @sapphire/result to ^2.3.3 ([4c6891b](https://github.com/sapphiredev/cli/commit/4c6891b28134120969975b00ab474683eaa9cedd)) 175 | - **deps:** Update dependency @sapphire/result to ^2.1.1 ([e42c188](https://github.com/sapphiredev/cli/commit/e42c188b05ec2f4a68403f94cfa6333e9d5421fd)) 176 | - **deps:** Update dependency @sapphire/result to v2 (#135) ([025c7ca](https://github.com/sapphiredev/cli/commit/025c7caed86e17e4b9e20def743e7c33d9b81589)) 177 | 178 | ## 📝 Documentation 179 | 180 | - Add @boingtheboeing as a contributor ([ac6088c](https://github.com/sapphiredev/cli/commit/ac6088c557800b25f3da1bd561de3941298e5f22)) 181 | 182 | ## 🚀 Features 183 | 184 | - Add templates for slash commands and context menu commands (#141) ([b97aeac](https://github.com/sapphiredev/cli/commit/b97aeac80999e81a3ae80e0dc7c749d6474945a8)) 185 | 186 | # [1.3.0](https://github.com/sapphiredev/cli/compare/v1.2.0...v1.3.0) - (2022-08-21) 187 | 188 | ## 🏠 Refactor 189 | 190 | - Switch to @favware/colorette-spinner ([e52962d](https://github.com/sapphiredev/cli/commit/e52962d53bc11af482c4ba60186f411d94f29b0b)) 191 | 192 | ## 🐛 Bug Fixes 193 | 194 | - **deps:** Update dependency @sapphire/result to ^2.3.3 ([4c6891b](https://github.com/sapphiredev/cli/commit/4c6891b28134120969975b00ab474683eaa9cedd)) 195 | - **deps:** Update dependency @sapphire/result to ^2.1.1 ([e42c188](https://github.com/sapphiredev/cli/commit/e42c188b05ec2f4a68403f94cfa6333e9d5421fd)) 196 | - **deps:** Update dependency @sapphire/result to v2 (#135) ([025c7ca](https://github.com/sapphiredev/cli/commit/025c7caed86e17e4b9e20def743e7c33d9b81589)) 197 | 198 | ## 📝 Documentation 199 | 200 | - Add @boingtheboeing as a contributor ([ac6088c](https://github.com/sapphiredev/cli/commit/ac6088c557800b25f3da1bd561de3941298e5f22)) 201 | 202 | ## 🚀 Features 203 | 204 | - Add templates for slash commands and context menu commands (#141) ([b97aeac](https://github.com/sapphiredev/cli/commit/b97aeac80999e81a3ae80e0dc7c749d6474945a8)) 205 | 206 | ## [1.2.0](https://github.com/sapphiredev/cli/compare/v1.1.0...v1.2.0) (2022-02-26) 207 | 208 | ### Features 209 | 210 | - add yarn v3 support ([#84](https://github.com/sapphiredev/cli/issues/84)) ([4c47d1a](https://github.com/sapphiredev/cli/commit/4c47d1aef07b600c0727106bd8d008213f3c2d04)) 211 | 212 | ## [1.1.0](https://github.com/sapphiredev/cli/compare/v1.0.2...v1.1.0) (2022-01-31) 213 | 214 | ### Features 215 | 216 | - add tsup and swc template options ([#71](https://github.com/sapphiredev/cli/issues/71)) ([625dd8e](https://github.com/sapphiredev/cli/commit/625dd8ea9d43f7005c72212fb5a65bf0b8aa7492)) 217 | 218 | ### Bug Fixes 219 | 220 | - **deps:** update dependency commander to v9 ([#76](https://github.com/sapphiredev/cli/issues/76)) ([d75ea1b](https://github.com/sapphiredev/cli/commit/d75ea1b1542490fd67a14630ee67e3223dc3b6e7)) 221 | - Generated command comportnent code has error ([#67](https://github.com/sapphiredev/cli/issues/67)) ([9901517](https://github.com/sapphiredev/cli/commit/990151771e3d1da09dee34c9995d70abc241a769)) 222 | 223 | ### [1.0.2](https://github.com/sapphiredev/cli/compare/v1.0.1...v1.0.2) (2021-11-08) 224 | 225 | ### Bug Fixes 226 | 227 | - update URL for guide ([0b7e402](https://github.com/sapphiredev/cli/commit/0b7e402e3db26af818824f423059c643374ed920)) 228 | 229 | ### [1.0.1](https://github.com/sapphiredev/cli/compare/v1.0.0...v1.0.1) (2021-11-06) 230 | 231 | ### Bug Fixes 232 | 233 | - allow more node & npm versions in engines field ([ce6c97f](https://github.com/sapphiredev/cli/commit/ce6c97f8c2934796e9d6ab159195f2c0fa05188a)) 234 | - **deps:** update all non-major dependencies ([#36](https://github.com/sapphiredev/cli/issues/36)) ([1a9e791](https://github.com/sapphiredev/cli/commit/1a9e791768ebbe5edd11875ac07c31b9d3cec50e)) 235 | - typo `msg` -> `message` ([#39](https://github.com/sapphiredev/cli/issues/39)) ([0f8933b](https://github.com/sapphiredev/cli/commit/0f8933b1af3927c96a79a1f4d9b1bcc46727dd24)) 236 | 237 | ## [1.0.0](https://github.com/sapphiredev/cli/compare/v0.0.3...v1.0.0) (2021-10-16) 238 | 239 | ### [0.0.3](https://github.com/sapphiredev/cli/compare/v0.0.2...v0.0.3) (2021-10-16) 240 | 241 | ### Bug Fixes 242 | 243 | - **templates:** Overridden run method to messageRun ([#30](https://github.com/sapphiredev/cli/issues/30)) ([07fc1d5](https://github.com/sapphiredev/cli/commit/07fc1d5516f057cd346340de853ded314741335f)) 244 | 245 | ### [0.0.2](https://github.com/sapphiredev/cli/compare/v0.0.1...v0.0.2) (2021-10-16) 246 | 247 | ### Features 248 | 249 | - add `init` command ([588e956](https://github.com/sapphiredev/cli/commit/588e956eeb9867be1e16db9bcd962fd72864d8fc)) 250 | - add JSON scheme for CLI config ([1ca569b](https://github.com/sapphiredev/cli/commit/1ca569b8ed89a869af3d5e39c6f1f4cc988edf08)) 251 | - add prompt for `init` command ([67d5a10](https://github.com/sapphiredev/cli/commit/67d5a106c66df3235260810ac6770234a7e7f2fc)) 252 | - category support ([0498b31](https://github.com/sapphiredev/cli/commit/0498b3125767b1b37614e50795402dbb8d72627e)) 253 | - switch to commander ([#15](https://github.com/sapphiredev/cli/issues/15)) ([8f34fa8](https://github.com/sapphiredev/cli/commit/8f34fa8323a6dfdb79abf2ebaf7fdd4d17f3df4b)) 254 | - YAML support ([#17](https://github.com/sapphiredev/cli/issues/17)) ([f69ae95](https://github.com/sapphiredev/cli/commit/f69ae959b664a3aa4342cf67c27802e197505c08)) 255 | 256 | ### Bug Fixes 257 | 258 | - add timeout when finding the config file ([46e3e21](https://github.com/sapphiredev/cli/commit/46e3e21e2b3e0d431e1eeea612a5da9b636ed34a)) 259 | - create the config file on project root instead of the current folder ([aa3b352](https://github.com/sapphiredev/cli/commit/aa3b352aa22b7cecb2117ced37f8e33202fa492b)) 260 | - include `templates` directory in the npm package ([c85406f](https://github.com/sapphiredev/cli/commit/c85406f9e9ba764a8063f5b7af458eb8b8f23924)) 261 | - path and executable issues on Windows ([f317d7f](https://github.com/sapphiredev/cli/commit/f317d7f35d39d388798e6250d79915e7cfdfa23a)) 262 | - **templates:** typescript types ([3812b34](https://github.com/sapphiredev/cli/commit/3812b34f7467e30624c7993a64366d2cb0821ac9)) 263 | 264 | ### 0.0.1 (2021-09-23) 265 | 266 | ### Features 267 | 268 | - add a function to check if a command exists ([107ccee](https://github.com/sapphiredev/cli/commit/107ccee0b55b3ddf6261d177d6b0b5730512811c)) 269 | - add command: `generate` ([b14a965](https://github.com/sapphiredev/cli/commit/b14a965548f5548ce7ac5e0e53d23c85ca827da2)) 270 | - add command: `new` ([4afa8cd](https://github.com/sapphiredev/cli/commit/4afa8cdaf122bac4bc7b16a8f18a9bda40663d4c)) 271 | - add command: `new` ([ffe5a69](https://github.com/sapphiredev/cli/commit/ffe5a695c0126523a4c9e79ea4817542023de193)) 272 | - add function to create files using templates ([e95db50](https://github.com/sapphiredev/cli/commit/e95db50555a9f02e241bea9753506e12a14b0395)) 273 | - add path aliases ([1b65110](https://github.com/sapphiredev/cli/commit/1b65110d0135f4ee9a064d8ac6f130f06ec8ab4c)) 274 | - add PWSH script for Windows ([c69c55f](https://github.com/sapphiredev/cli/commit/c69c55f44f39b6d1984adaded486e29759118375)) 275 | - add templates for `generate` command ([be6f535](https://github.com/sapphiredev/cli/commit/be6f53583ecba3d959a76a12310ff3d80cbd52dd)) 276 | - update batch file for Windows Command Prompt ([746ab83](https://github.com/sapphiredev/cli/commit/746ab838bc668c863a8fc6b2d9632e4c5790acdc)) 277 | - update config template ([65c47a0](https://github.com/sapphiredev/cli/commit/65c47a0728f4652b725b37ebcf758fb60155de7c)) 278 | - use new template format ([1adcbeb](https://github.com/sapphiredev/cli/commit/1adcbeb39678fbbfe6ea119dd9586d3a022cbc11)) 279 | 280 | ### Bug Fixes 281 | 282 | - language value was not getting replaced when creating the `.sapphirerc.json` file from template ([946a40a](https://github.com/sapphiredev/cli/commit/946a40a4365411c2f20a1ed8806e0057c0afbc56)) 283 | --------------------------------------------------------------------------------