create-gramstax 0.8.24 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,4 +10,4 @@ $$ | $$ |$$ | $$ __$$ |$$ | $$ | $$ | \\____$$\\ $$ |$$\\ $$ __$$ | $$
10
10
  \\______/ \\__| \\_______|\\__| \\__| \\__|\\_______/ \\____/ \\_______|\\__/ \\__|
11
11
 
12
12
  `,t=this.version?`v${this.version}`:"v0.0.0";console.log(`${e}${t}
13
- `)}getPackageManagerCommands(e){return{bun:{install:"bun install",dev:"bun dev",start:"bun start"},npm:{install:"npm install",dev:"npm run dev",start:"npm start"},pnpm:{install:"pnpm install",dev:"pnpm dev",start:"pnpm start"},yarn:{install:"yarn install",dev:"yarn dev",start:"yarn start"}}[e]}copyDirectorySync(e,t,n={}){i.existsSync(t)||i.mkdirSync(t,{recursive:!0});let a=i.readdirSync(e,{withFileTypes:!0});for(let c of a){let m=s.join(e,c.name),l=s.join(t,c.name);if(c.isDirectory())this.copyDirectorySync(m,l,n);else if(c.isFile()){let $=i.readFileSync(m,"utf-8");$=this.replaceTemplateVariables($,n),i.writeFileSync(l,$)}}}replaceTemplateVariables(e,t){let n=e;for(let[a,c]of Object.entries(t))n=n.replace(new RegExp(`{{${a}}}`,"g"),c);return n}async promptForProjectName(){return(await P.default.prompt({type:"input",name:"projectName",message:"What is your project named?",initial:"",validate:p(t=>t===""||/^[a-z0-9-_]+$/i.test(t)?!0:"Project name can only contain letters, numbers, hyphens, and underscores","validate")})).projectName}async promptForConfiguration(){let e=[];e.push({type:"select",name:"packageManager",message:"Which package manager would you like to use?",choices:[{name:"bun",message:"Bun (recommended)",value:"bun"},{name:"npm",message:"npm",value:"npm"},{name:"pnpm",message:"pnpm",value:"pnpm"},{name:"yarn",message:"Yarn",value:"yarn"}],initial:0}),e.push({type:"select",name:"runtime",message:"Which runtime would you like to use?",choices:[{name:"bun",message:"Bun (recommended)",value:"bun"},{name:"node",message:"Node.js",value:"node"}],initial:0}),e.push({type:"confirm",name:"eslint",message:"Would you like to use ESLint?",initial:!0});let t=await P.default.prompt(e);return{packageManager:t.packageManager,runtime:t.runtime,eslint:t.eslint}}async installDependencies(e,t){return await new Promise(n=>{let a=g.resolvePackageManagerCommand(t),m=(0,j.spawn)(a,["install"],{cwd:e,stdio:"inherit",shell:!1});m.on("close",l=>{l===0?n(!0):(r.fail("Dependencies installation failed"),n(!1))}),m.on("error",l=>{r.fail(`Dependencies installation failed: ${l.message}`),n(!1)})})}async gatherProjectConfig(){this.printBanner();let e=this.projectDirectory,t,n=!1,a=g.resolveInitialWorkingDirectory();e?(t=s.resolve(a,e),n=!1):(e=await this.promptForProjectName(),e===""?(e=s.basename(a),t=a,n=!0):(t=s.resolve(a,e),n=!1));let c=await this.promptForConfiguration(),m=c.packageManager,l=c.runtime,$=c.eslint;return r.info(`Project: ${e}`),r.info(`Location: ${t}`),r.info(`Package Manager: ${m}`),r.info(`Runtime: ${l}`),r.info(`ESLint: ${$?"Yes":"No"}`),n?g.isDirectoryEffectivelyEmpty(t)||(r.error("Current directory is not empty!"),process.exit(1)):i.existsSync(t)&&(r.error(`Directory "${e}" already exists!`),process.exit(1)),{projectName:e,projectPath:t,packageManager:m,runtime:l,setupEslint:$}}async executeProjectGeneration(e){let{projectName:t,projectPath:n,packageManager:a,runtime:c,setupEslint:m}=e;r.info("Creating project directory.."),i.existsSync(n)||i.mkdirSync(n,{recursive:!0}),r.info("Copying template files..");let l=s.join(Y,"templates"),$=this.getPackageManagerCommands(a),I={projectName:t,installCmd:$.install,runDevCmd:$.dev,runStartCmd:$.start};this.copyDirectorySync(l,n,I),r.info("Creating logs directory.."),i.mkdirSync(s.join(n,"logs"),{recursive:!0}),i.writeFileSync(s.join(n,"logs/.gitkeep"),"");let L=s.join(n,".env"),S=s.join(n,".env.example");if(i.existsSync(S)&&(r.info("Creating .env file.."),i.renameSync(S,L)),c!=="bun"){r.info("Removing bunfig.toml for Node.js runtime..");let d=s.join(n,"bunfig.toml");i.existsSync(d)&&i.unlinkSync(d)}if(r.info("Installing dependencies.."),!await this.installDependencies(n,a))return r.fail("Project created failed!"),process.exit();let u=s.join(n,"eslint.config.ts.template"),T=s.join(n,"eslint.config.ts");if(m){r.info("Installing ESLint dependencies..");let d=g.resolvePackageManagerCommand(a),b=a==="yarn"?["add","-D"]:["install","-D"];b.push("eslint","@eslint/js","globals","typescript-eslint"),await new Promise(k=>{let w=(0,j.spawn)(d,b,{cwd:n,stdio:"inherit",shell:!1});w.on("close",()=>k()),w.on("error",U=>{r.fail(`Failed to install ESLint dependencies: ${U.message}`),k()})}),i.existsSync(u)&&i.renameSync(u,T)}else i.existsSync(u)&&i.unlinkSync(u);r.info("Project created successfully!"),r.info(`Next steps: ${e.projectPath!==process.cwd()?`cd ${t} && `:" "}${$.install} && ${$.dev}`),r.info("Done..")}async createProject(){try{this.config=await this.gatherProjectConfig(),await this.executeProjectGeneration(this.config)}catch(e){e instanceof Error?r.error(`Failed to create project: ${e.message}`):r.error(`Failed to create project: ${String(e)}`),process.exit(1)}}};var N=require("commander"),R=require("fs"),y=require("path"),M=require("url"),z=(0,M.fileURLToPath)(f),H=(0,y.dirname)(z),K=(0,y.join)(H,"../../package.json"),x=JSON.parse((0,R.readFileSync)(K,"utf-8")).version,F=new N.Command;F.name("create-gramstax").description("CLI to create a new Gramstax Telegram bot project").version(x).argument("[project-directory]","Directory to create the project in").action(async o=>{try{await new _(o,{version:x}).createProject()}catch(e){r.error(`Failed to create project: ${String(e)}`),process.exit(1)}});F.parse();
13
+ `)}getPackageManagerCommands(e){return{bun:{install:"bun install",dev:"bun dev",start:"bun start"},npm:{install:"npm install",dev:"npm run dev",start:"npm start"},pnpm:{install:"pnpm install",dev:"pnpm dev",start:"pnpm start"},yarn:{install:"yarn install",dev:"yarn dev",start:"yarn start"}}[e]}copyDirectorySync(e,t,n={}){i.existsSync(t)||i.mkdirSync(t,{recursive:!0});let a=i.readdirSync(e,{withFileTypes:!0});for(let c of a){let m=s.join(e,c.name),l=s.join(t,c.name);if(c.isDirectory())this.copyDirectorySync(m,l,n);else if(c.isFile()){let $=i.readFileSync(m,"utf-8");$=this.replaceTemplateVariables($,n),i.writeFileSync(l,$)}}}replaceTemplateVariables(e,t){let n=e;for(let[a,c]of Object.entries(t))n=n.replace(new RegExp(`{{${a}}}`,"g"),c);return n}async promptForProjectName(){return(await P.default.prompt({type:"input",name:"projectName",message:"What is your project named?",initial:"",validate:p(t=>t===""||/^[a-z0-9-_]+$/i.test(t)?!0:"Project name can only contain letters, numbers, hyphens, and underscores","validate")})).projectName}async promptForConfiguration(){let e=[];e.push({type:"select",name:"packageManager",message:"Which package manager would you like to use?",choices:[{name:"bun",message:"Bun (recommended)",value:"bun"},{name:"npm",message:"npm",value:"npm"},{name:"pnpm",message:"pnpm",value:"pnpm"},{name:"yarn",message:"Yarn",value:"yarn"}],initial:0}),e.push({type:"select",name:"runtime",message:"Which runtime would you like to use?",choices:[{name:"bun",message:"Bun (recommended)",value:"bun"},{name:"node",message:"Node.js",value:"node"}],initial:0}),e.push({type:"confirm",name:"eslint",message:"Would you like to use ESLint?",initial:!0});let t=await P.default.prompt(e);return{packageManager:t.packageManager,runtime:t.runtime,eslint:t.eslint}}async installDependencies(e,t){return await new Promise(n=>{let a=g.resolvePackageManagerCommand(t),m=(0,j.spawn)(a,["install"],{cwd:e,stdio:"inherit",shell:!1});m.on("close",l=>{l===0?n(!0):(r.fail("Dependencies installation failed"),n(!1))}),m.on("error",l=>{r.fail(`Dependencies installation failed: ${l.message}`),n(!1)})})}async gatherProjectConfig(){this.printBanner();let e=this.projectDirectory,t,n=!1,a=g.resolveInitialWorkingDirectory();e?(t=s.resolve(a,e),n=!1):(e=await this.promptForProjectName(),e===""?(e=s.basename(a),t=a,n=!0):(t=s.resolve(a,e),n=!1));let c=await this.promptForConfiguration(),m=c.packageManager,l=c.runtime,$=c.eslint;if(r.info(`Project: ${e}`),r.info(`Location: ${t}`),r.info(`Package Manager: ${m}`),r.info(`Runtime: ${l}`),r.info(`ESLint: ${$?"Yes":"No"}`),n){if(!g.isDirectoryEffectivelyEmpty(t))throw r.error("Current directory is not empty!"),new Error("Current directory is not empty")}else if(i.existsSync(t))throw r.error(`Directory "${e}" already exists!`),new Error(`Directory "${e}" already exists`);return{projectName:e,projectPath:t,packageManager:m,runtime:l,setupEslint:$}}async executeProjectGeneration(e){let{projectName:t,projectPath:n,packageManager:a,runtime:c,setupEslint:m}=e;r.info("Creating project directory.."),i.existsSync(n)||i.mkdirSync(n,{recursive:!0}),r.info("Copying template files..");let l=s.join(Y,"templates"),$=this.getPackageManagerCommands(a),I={projectName:t,installCmd:$.install,runDevCmd:$.dev,runStartCmd:$.start};this.copyDirectorySync(l,n,I),r.info("Creating logs directory.."),i.mkdirSync(s.join(n,"logs"),{recursive:!0}),i.writeFileSync(s.join(n,"logs/.gitkeep"),"");let L=s.join(n,".env"),S=s.join(n,".env.example");if(i.existsSync(S)&&(r.info("Creating .env file.."),i.renameSync(S,L)),c!=="bun"){r.info("Removing bunfig.toml for Node.js runtime..");let d=s.join(n,"bunfig.toml");i.existsSync(d)&&i.unlinkSync(d)}if(r.info("Installing dependencies.."),!await this.installDependencies(n,a))throw new Error("Project dependencies installation failed");let u=s.join(n,"eslint.config.ts.template"),T=s.join(n,"eslint.config.ts");if(m){r.info("Installing ESLint dependencies..");let d=g.resolvePackageManagerCommand(a),w=a==="yarn"?["add","-D"]:["install","-D"];w.push("eslint","@eslint/js","globals","typescript-eslint"),await new Promise(b=>{let k=(0,j.spawn)(d,w,{cwd:n,stdio:"inherit",shell:!1});k.on("close",()=>b()),k.on("error",U=>{r.fail(`Failed to install ESLint dependencies: ${U.message}`),b()})}),i.existsSync(u)&&i.renameSync(u,T)}else i.existsSync(u)&&i.unlinkSync(u);r.info("Project created successfully!"),r.info(`Next steps: ${e.projectPath!==process.cwd()?`cd ${t} && `:" "}${$.install} && ${$.dev}`),r.info("Done..")}async createProject(){try{this.config=await this.gatherProjectConfig(),await this.executeProjectGeneration(this.config)}catch(e){e instanceof Error?r.error(`Failed to create project: ${e.message}`):r.error(`Failed to create project: ${String(e)}`),process.exit(1)}}};var N=require("commander"),R=require("fs"),y=require("path"),M=require("url"),z=(0,M.fileURLToPath)(f),H=(0,y.dirname)(z),K=(0,y.join)(H,"../../package.json"),x=JSON.parse((0,R.readFileSync)(K,"utf-8")).version,F=new N.Command;F.name("create-gramstax").description("CLI to create a new Gramstax Telegram bot project").version(x).argument("[project-directory]","Directory to create the project in").action(async o=>{try{await new _(o,{version:x}).createProject()}catch(e){r.error(`Failed to create project: ${String(e)}`),process.exit(1)}});F.parse();
package/dist/src/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- var E=Object.defineProperty;var g=(p,e)=>E(p,"name",{value:e,configurable:!0});import{LoggingPretty as N}from"logging-pretty";var r=new N;import*as i from"fs";import*as a from"path";import{fileURLToPath as R}from"url";import{readdirSync as F}from"fs";var m=class p{static{g(this,"FsUt")}static DEFAULT_IGNORED_ENTRIES=new Set([".git",".gitignore",".gitkeep",".DS_Store","Thumbs.db"]);static isDirectoryEffectivelyEmpty(e,t=p.DEFAULT_IGNORED_ENTRIES){return F(e).filter(o=>!t.has(o)).length===0}static resolvePackageManagerCommand(e,t=process.platform){return t!=="win32"||e==="bun"?e:`${e}.cmd`}static resolveInitialWorkingDirectory(e=process.cwd(),t=process.env.INIT_CWD){return t??e}};import{spawn as j}from"child_process";import P from"enquirer";var L=R(import.meta.url),T=a.dirname(L),d=class{constructor(e,t){this.projectDirectory=e;this.version=t?.version}projectDirectory;static{g(this,"Create")}config;version;printBanner(){let e=`
2
+ var C=Object.defineProperty;var g=(p,e)=>C(p,"name",{value:e,configurable:!0});import{LoggingPretty as N}from"logging-pretty";var r=new N;import*as i from"fs";import*as a from"path";import{fileURLToPath as R}from"url";import{readdirSync as F}from"fs";var m=class p{static{g(this,"FsUt")}static DEFAULT_IGNORED_ENTRIES=new Set([".git",".gitignore",".gitkeep",".DS_Store","Thumbs.db"]);static isDirectoryEffectivelyEmpty(e,t=p.DEFAULT_IGNORED_ENTRIES){return F(e).filter(o=>!t.has(o)).length===0}static resolvePackageManagerCommand(e,t=process.platform){return t!=="win32"||e==="bun"?e:`${e}.cmd`}static resolveInitialWorkingDirectory(e=process.cwd(),t=process.env.INIT_CWD){return t??e}};import{spawn as j}from"child_process";import P from"enquirer";var L=R(import.meta.url),T=a.dirname(L),d=class{constructor(e,t){this.projectDirectory=e;this.version=t?.version}projectDirectory;static{g(this,"Create")}config;version;printBanner(){let e=`
3
3
  $$$$$$\\ $$\\
4
4
  $$ __$$\\ $$ |
5
5
  $$ / \\__| $$$$$$\\ $$$$$$\\ $$$$$$\\$$$$\\ $$$$$$$\\ $$$$$$\\ $$$$$$\\ $$\\ $$\\
@@ -10,4 +10,4 @@ $$ | $$ |$$ | $$ __$$ |$$ | $$ | $$ | \\____$$\\ $$ |$$\\ $$ __$$ | $$
10
10
  \\______/ \\__| \\_______|\\__| \\__| \\__|\\_______/ \\____/ \\_______|\\__/ \\__|
11
11
 
12
12
  `,t=this.version?`v${this.version}`:"v0.0.0";console.log(`${e}${t}
13
- `)}getPackageManagerCommands(e){return{bun:{install:"bun install",dev:"bun dev",start:"bun start"},npm:{install:"npm install",dev:"npm run dev",start:"npm start"},pnpm:{install:"pnpm install",dev:"pnpm dev",start:"pnpm start"},yarn:{install:"yarn install",dev:"yarn dev",start:"yarn start"}}[e]}copyDirectorySync(e,t,n={}){i.existsSync(t)||i.mkdirSync(t,{recursive:!0});let s=i.readdirSync(e,{withFileTypes:!0});for(let o of s){let l=a.join(e,o.name),$=a.join(t,o.name);if(o.isDirectory())this.copyDirectorySync(l,$,n);else if(o.isFile()){let c=i.readFileSync(l,"utf-8");c=this.replaceTemplateVariables(c,n),i.writeFileSync($,c)}}}replaceTemplateVariables(e,t){let n=e;for(let[s,o]of Object.entries(t))n=n.replace(new RegExp(`{{${s}}}`,"g"),o);return n}async promptForProjectName(){return(await P.prompt({type:"input",name:"projectName",message:"What is your project named?",initial:"",validate:g(t=>t===""||/^[a-z0-9-_]+$/i.test(t)?!0:"Project name can only contain letters, numbers, hyphens, and underscores","validate")})).projectName}async promptForConfiguration(){let e=[];e.push({type:"select",name:"packageManager",message:"Which package manager would you like to use?",choices:[{name:"bun",message:"Bun (recommended)",value:"bun"},{name:"npm",message:"npm",value:"npm"},{name:"pnpm",message:"pnpm",value:"pnpm"},{name:"yarn",message:"Yarn",value:"yarn"}],initial:0}),e.push({type:"select",name:"runtime",message:"Which runtime would you like to use?",choices:[{name:"bun",message:"Bun (recommended)",value:"bun"},{name:"node",message:"Node.js",value:"node"}],initial:0}),e.push({type:"confirm",name:"eslint",message:"Would you like to use ESLint?",initial:!0});let t=await P.prompt(e);return{packageManager:t.packageManager,runtime:t.runtime,eslint:t.eslint}}async installDependencies(e,t){return await new Promise(n=>{let s=m.resolvePackageManagerCommand(t),l=j(s,["install"],{cwd:e,stdio:"inherit",shell:!1});l.on("close",$=>{$===0?n(!0):(r.fail("Dependencies installation failed"),n(!1))}),l.on("error",$=>{r.fail(`Dependencies installation failed: ${$.message}`),n(!1)})})}async gatherProjectConfig(){this.printBanner();let e=this.projectDirectory,t,n=!1,s=m.resolveInitialWorkingDirectory();e?(t=a.resolve(s,e),n=!1):(e=await this.promptForProjectName(),e===""?(e=a.basename(s),t=s,n=!0):(t=a.resolve(s,e),n=!1));let o=await this.promptForConfiguration(),l=o.packageManager,$=o.runtime,c=o.eslint;return r.info(`Project: ${e}`),r.info(`Location: ${t}`),r.info(`Package Manager: ${l}`),r.info(`Runtime: ${$}`),r.info(`ESLint: ${c?"Yes":"No"}`),n?m.isDirectoryEffectivelyEmpty(t)||(r.error("Current directory is not empty!"),process.exit(1)):i.existsSync(t)&&(r.error(`Directory "${e}" already exists!`),process.exit(1)),{projectName:e,projectPath:t,packageManager:l,runtime:$,setupEslint:c}}async executeProjectGeneration(e){let{projectName:t,projectPath:n,packageManager:s,runtime:o,setupEslint:l}=e;r.info("Creating project directory.."),i.existsSync(n)||i.mkdirSync(n,{recursive:!0}),r.info("Copying template files..");let $=a.join(T,"templates"),c=this.getPackageManagerCommands(s),b={projectName:t,installCmd:c.install,runDevCmd:c.dev,runStartCmd:c.start};this.copyDirectorySync($,n,b),r.info("Creating logs directory.."),i.mkdirSync(a.join(n,"logs"),{recursive:!0}),i.writeFileSync(a.join(n,"logs/.gitkeep"),"");let w=a.join(n,".env"),_=a.join(n,".env.example");if(i.existsSync(_)&&(r.info("Creating .env file.."),i.renameSync(_,w)),o!=="bun"){r.info("Removing bunfig.toml for Node.js runtime..");let u=a.join(n,"bunfig.toml");i.existsSync(u)&&i.unlinkSync(u)}if(r.info("Installing dependencies.."),!await this.installDependencies(n,s))return r.fail("Project created failed!"),process.exit();let f=a.join(n,"eslint.config.ts.template"),D=a.join(n,"eslint.config.ts");if(l){r.info("Installing ESLint dependencies..");let u=m.resolvePackageManagerCommand(s),y=s==="yarn"?["add","-D"]:["install","-D"];y.push("eslint","@eslint/js","globals","typescript-eslint"),await new Promise(h=>{let v=j(u,y,{cwd:n,stdio:"inherit",shell:!1});v.on("close",()=>h()),v.on("error",C=>{r.fail(`Failed to install ESLint dependencies: ${C.message}`),h()})}),i.existsSync(f)&&i.renameSync(f,D)}else i.existsSync(f)&&i.unlinkSync(f);r.info("Project created successfully!"),r.info(`Next steps: ${e.projectPath!==process.cwd()?`cd ${t} && `:" "}${c.install} && ${c.dev}`),r.info("Done..")}async createProject(){try{this.config=await this.gatherProjectConfig(),await this.executeProjectGeneration(this.config)}catch(e){e instanceof Error?r.error(`Failed to create project: ${e.message}`):r.error(`Failed to create project: ${String(e)}`),process.exit(1)}}};import{Command as I}from"commander";import{readFileSync as W}from"fs";import{dirname as A,join as G}from"path";import{fileURLToPath as B}from"url";var O=B(import.meta.url),U=A(O),V=G(U,"../../package.json"),k=JSON.parse(W(V,"utf-8")).version,S=new I;S.name("create-gramstax").description("CLI to create a new Gramstax Telegram bot project").version(k).argument("[project-directory]","Directory to create the project in").action(async p=>{try{await new d(p,{version:k}).createProject()}catch(e){r.error(`Failed to create project: ${String(e)}`),process.exit(1)}});S.parse();
13
+ `)}getPackageManagerCommands(e){return{bun:{install:"bun install",dev:"bun dev",start:"bun start"},npm:{install:"npm install",dev:"npm run dev",start:"npm start"},pnpm:{install:"pnpm install",dev:"pnpm dev",start:"pnpm start"},yarn:{install:"yarn install",dev:"yarn dev",start:"yarn start"}}[e]}copyDirectorySync(e,t,n={}){i.existsSync(t)||i.mkdirSync(t,{recursive:!0});let s=i.readdirSync(e,{withFileTypes:!0});for(let o of s){let l=a.join(e,o.name),$=a.join(t,o.name);if(o.isDirectory())this.copyDirectorySync(l,$,n);else if(o.isFile()){let c=i.readFileSync(l,"utf-8");c=this.replaceTemplateVariables(c,n),i.writeFileSync($,c)}}}replaceTemplateVariables(e,t){let n=e;for(let[s,o]of Object.entries(t))n=n.replace(new RegExp(`{{${s}}}`,"g"),o);return n}async promptForProjectName(){return(await P.prompt({type:"input",name:"projectName",message:"What is your project named?",initial:"",validate:g(t=>t===""||/^[a-z0-9-_]+$/i.test(t)?!0:"Project name can only contain letters, numbers, hyphens, and underscores","validate")})).projectName}async promptForConfiguration(){let e=[];e.push({type:"select",name:"packageManager",message:"Which package manager would you like to use?",choices:[{name:"bun",message:"Bun (recommended)",value:"bun"},{name:"npm",message:"npm",value:"npm"},{name:"pnpm",message:"pnpm",value:"pnpm"},{name:"yarn",message:"Yarn",value:"yarn"}],initial:0}),e.push({type:"select",name:"runtime",message:"Which runtime would you like to use?",choices:[{name:"bun",message:"Bun (recommended)",value:"bun"},{name:"node",message:"Node.js",value:"node"}],initial:0}),e.push({type:"confirm",name:"eslint",message:"Would you like to use ESLint?",initial:!0});let t=await P.prompt(e);return{packageManager:t.packageManager,runtime:t.runtime,eslint:t.eslint}}async installDependencies(e,t){return await new Promise(n=>{let s=m.resolvePackageManagerCommand(t),l=j(s,["install"],{cwd:e,stdio:"inherit",shell:!1});l.on("close",$=>{$===0?n(!0):(r.fail("Dependencies installation failed"),n(!1))}),l.on("error",$=>{r.fail(`Dependencies installation failed: ${$.message}`),n(!1)})})}async gatherProjectConfig(){this.printBanner();let e=this.projectDirectory,t,n=!1,s=m.resolveInitialWorkingDirectory();e?(t=a.resolve(s,e),n=!1):(e=await this.promptForProjectName(),e===""?(e=a.basename(s),t=s,n=!0):(t=a.resolve(s,e),n=!1));let o=await this.promptForConfiguration(),l=o.packageManager,$=o.runtime,c=o.eslint;if(r.info(`Project: ${e}`),r.info(`Location: ${t}`),r.info(`Package Manager: ${l}`),r.info(`Runtime: ${$}`),r.info(`ESLint: ${c?"Yes":"No"}`),n){if(!m.isDirectoryEffectivelyEmpty(t))throw r.error("Current directory is not empty!"),new Error("Current directory is not empty")}else if(i.existsSync(t))throw r.error(`Directory "${e}" already exists!`),new Error(`Directory "${e}" already exists`);return{projectName:e,projectPath:t,packageManager:l,runtime:$,setupEslint:c}}async executeProjectGeneration(e){let{projectName:t,projectPath:n,packageManager:s,runtime:o,setupEslint:l}=e;r.info("Creating project directory.."),i.existsSync(n)||i.mkdirSync(n,{recursive:!0}),r.info("Copying template files..");let $=a.join(T,"templates"),c=this.getPackageManagerCommands(s),S={projectName:t,installCmd:c.install,runDevCmd:c.dev,runStartCmd:c.start};this.copyDirectorySync($,n,S),r.info("Creating logs directory.."),i.mkdirSync(a.join(n,"logs"),{recursive:!0}),i.writeFileSync(a.join(n,"logs/.gitkeep"),"");let b=a.join(n,".env"),y=a.join(n,".env.example");if(i.existsSync(y)&&(r.info("Creating .env file.."),i.renameSync(y,b)),o!=="bun"){r.info("Removing bunfig.toml for Node.js runtime..");let u=a.join(n,"bunfig.toml");i.existsSync(u)&&i.unlinkSync(u)}if(r.info("Installing dependencies.."),!await this.installDependencies(n,s))throw new Error("Project dependencies installation failed");let f=a.join(n,"eslint.config.ts.template"),D=a.join(n,"eslint.config.ts");if(l){r.info("Installing ESLint dependencies..");let u=m.resolvePackageManagerCommand(s),_=s==="yarn"?["add","-D"]:["install","-D"];_.push("eslint","@eslint/js","globals","typescript-eslint"),await new Promise(h=>{let v=j(u,_,{cwd:n,stdio:"inherit",shell:!1});v.on("close",()=>h()),v.on("error",E=>{r.fail(`Failed to install ESLint dependencies: ${E.message}`),h()})}),i.existsSync(f)&&i.renameSync(f,D)}else i.existsSync(f)&&i.unlinkSync(f);r.info("Project created successfully!"),r.info(`Next steps: ${e.projectPath!==process.cwd()?`cd ${t} && `:" "}${c.install} && ${c.dev}`),r.info("Done..")}async createProject(){try{this.config=await this.gatherProjectConfig(),await this.executeProjectGeneration(this.config)}catch(e){e instanceof Error?r.error(`Failed to create project: ${e.message}`):r.error(`Failed to create project: ${String(e)}`),process.exit(1)}}};import{Command as I}from"commander";import{readFileSync as W}from"fs";import{dirname as A,join as G}from"path";import{fileURLToPath as B}from"url";var O=B(import.meta.url),U=A(O),V=G(U,"../../package.json"),k=JSON.parse(W(V,"utf-8")).version,w=new I;w.name("create-gramstax").description("CLI to create a new Gramstax Telegram bot project").version(k).argument("[project-directory]","Directory to create the project in").action(async p=>{try{await new d(p,{version:k}).createProject()}catch(e){r.error(`Failed to create project: ${String(e)}`),process.exit(1)}});w.parse();
@@ -4,8 +4,6 @@ A Telegram bot built with Gramstax framework.
4
4
 
5
5
  ## Getting Started
6
6
 
7
- ### Development
8
-
9
7
  1. Install dependencies:
10
8
 
11
9
  ```bash
@@ -22,12 +20,6 @@ TELEGRAM_BOT_TOKEN="<...your_bot_token_here...>"
22
20
 
23
21
  3. Run the development server:
24
22
 
25
- ```bash
26
- {{runDevCmd}}
27
- ```
28
-
29
- #### Using Bun directly
30
-
31
23
  ```bash
32
24
  {{runStartCmd}}
33
25
  ```
@@ -54,5 +46,3 @@ TELEGRAM_BOT_TOKEN="<...your_bot_token_here...>"
54
46
  ## Learn More
55
47
 
56
48
  - [Gramstax Documentation](https://github.com/gramstax/gramstax)
57
- - [Grammy Documentation](https://grammy.dev/)
58
- - [Telegram Bot API](https://core.telegram.org/bots/api)
@@ -0,0 +1,9 @@
1
+ import {describe, it, expect} from "bun:test"
2
+ import {bot} from "./setup"
3
+
4
+ describe(`E2E: BlankPage`, () => {
5
+ it(`load as a registered page in the bot`, () => {
6
+ const allPages = bot.pages.all
7
+ expect(allPages[`blank`]).toBeDefined()
8
+ })
9
+ })
@@ -0,0 +1,28 @@
1
+ import {describe, it, expect, beforeEach} from "bun:test"
2
+ import {e2e} from "./setup"
3
+
4
+ const intent = `general-status`
5
+ const asUser = {user: {id: 8001, username: `testuser`}, chat: {id: 8001}}
6
+
7
+ beforeEach(() => {
8
+ e2e.clearCalls()
9
+ })
10
+
11
+ describe(`E2E: GeneralStatusPage`, () => {
12
+ it(`show 'Route not found' for unrecognized callback query`, async () => {
13
+ await e2e.sendText(`/start`, asUser)
14
+ e2e.clearCalls()
15
+
16
+ const responses = await e2e.sendCallbackQuery(`nonexistent-callback-data`, asUser)
17
+ const responsesId = await e2e.sendCallbackQuery(`nonexistent-callback-data`, {user: {id: 8002, username: `pengguna`, language_code: `id`}, chat: {id: 8002}})
18
+
19
+ expect(responses.length).toBeGreaterThan(0)
20
+ expect(responsesId.length).toBeGreaterThan(0)
21
+ const reply = responses[0]!
22
+ const replyId = responsesId[0]!
23
+ expect(reply).toBeDefined()
24
+ expect(replyId).toBeDefined()
25
+ expect(reply.body?.text).toBe(e2e.gmsg(intent, `d`, {isNotFound: true}))
26
+ expect(replyId.body?.text).toBe(e2e.gmsg(intent, `d`, {isNotFound: true}, `id`))
27
+ })
28
+ })
@@ -0,0 +1,28 @@
1
+ import {describe, it, expect, beforeEach} from "bun:test"
2
+ import {e2e} from "./setup"
3
+
4
+ const intent = `help`
5
+ const asUser = {user: {id: 7001, username: `testuser`}, chat: {id: 7001}}
6
+
7
+ beforeEach(() => {
8
+ e2e.clearCalls()
9
+ })
10
+
11
+ describe(`E2E: HelpPage`, () => {
12
+ it(`show help text when navigating from /start -> Help`, async () => {
13
+ await e2e.sendText(`/start`, asUser)
14
+ e2e.clearCalls()
15
+
16
+ const responses = await e2e.sendCallbackQuery(intent, asUser)
17
+ const responsesId = await e2e.sendCallbackQuery(intent, {user: {id: 7002, username: `pengguna`, language_code: `id`}, chat: {id: 7002}})
18
+
19
+ expect(responses.length).toBeGreaterThan(0)
20
+ expect(responsesId.length).toBeGreaterThan(0)
21
+ const edit = responses[0]!
22
+ const editId = responsesId[0]!
23
+ expect(edit.body?.text).toBe(e2e.gmsg(intent, `s1`, {}))
24
+ expect(editId.body?.text).toBe(e2e.gmsg(intent, `s1`, {}, `id`))
25
+ expect(e2e.extractButtonLabels(edit)).toEqual(e2e.gkb(intent, `s1`, `inline`, {}))
26
+ expect(e2e.extractButtonLabels(editId)).toEqual(e2e.gkb(intent, `s1`, `inline`, {}, `id`))
27
+ })
28
+ })
@@ -0,0 +1,28 @@
1
+ import {describe, it, expect, beforeEach} from "bun:test"
2
+ import {e2e} from "./setup"
3
+
4
+ const intent = `random-number`
5
+ const asUser = {user: {id: 2003, username: `testuser`}, chat: {id: 2003}}
6
+
7
+ beforeEach(() => {
8
+ e2e.clearCalls()
9
+ })
10
+
11
+ describe(`E2E: RandomNumberPage`, () => {
12
+ it(`show generated random number when navigating from /start`, async () => {
13
+ await e2e.sendText(`/start`, asUser)
14
+ e2e.clearCalls()
15
+
16
+ const responses = await e2e.sendCallbackQuery(intent, asUser)
17
+ const responsesId = await e2e.sendCallbackQuery(intent, {user: {id: 6001, username: `pengguna`, language_code: `id`}, chat: {id: 6001}})
18
+
19
+ expect(responses.length).toBeGreaterThan(0)
20
+ expect(responsesId.length).toBeGreaterThan(0)
21
+ const edit = responses[0]!
22
+ const editId = responsesId[0]!
23
+ expect(edit.body?.text).toContain(`Random Number`)
24
+ expect(editId.body?.text).toContain(`Angka Acak`)
25
+ expect(e2e.extractButtonLabels(edit)).toEqual(e2e.gkb(intent, `s1`, `inline`, {}))
26
+ expect(e2e.extractButtonLabels(editId)).toEqual(e2e.gkb(intent, `s1`, `inline`, {}, `id`))
27
+ })
28
+ })
@@ -0,0 +1,15 @@
1
+ import {join} from "node:path"
2
+ import {BotCore} from "../src/core/bot"
3
+ import {TemplateManager} from "gramstax"
4
+
5
+ export type {ITE2eApiCall, ITE2eUpdateOptions} from "gramstax"
6
+
7
+ const templateManager = new TemplateManager({path: [join(import.meta.dir, `../src/pages`)]})
8
+ export const bot = new BotCore({
9
+ deploy: `test`,
10
+ token: `test`,
11
+ log: false,
12
+ templateManager
13
+ })
14
+
15
+ export const e2e = bot.e2e!
@@ -0,0 +1,33 @@
1
+ import {describe, it, expect, beforeEach} from "bun:test"
2
+ import {e2e} from "./setup"
3
+
4
+ const intent = `start`
5
+ const asUser = {user: {id: 2001, username: `testuser`}, chat: {id: 2001}}
6
+
7
+ beforeEach(() => {
8
+ e2e.clearCalls()
9
+ })
10
+
11
+ describe(`E2E: StartPage`, () => {
12
+ it(`respond to /start command with welcome message and full keyboard`, async () => {
13
+ const responses = await e2e.sendText(`/start`, asUser)
14
+ const responsesId = await e2e.sendText(`/start`, {user: {id: 5001, username: `pengguna`, language_code: `id`}, chat: {id: 5001}})
15
+
16
+ expect(responses.length).toBeGreaterThan(0)
17
+ expect(responsesId.length).toBeGreaterThan(0)
18
+ const reply = responses[0]!
19
+ const replyId = responsesId[0]!
20
+ expect(reply.body?.text).toContain(`Welcome`)
21
+ expect(replyId.body?.text).toContain(`Selamat datang`)
22
+ expect(e2e.extractButtonLabels(reply)).toEqual(e2e.gkb(intent, `s1`, `inline`, {}))
23
+ expect(e2e.extractButtonLabels(replyId)).toEqual(e2e.gkb(intent, `s1`, `inline`, {}, `id`))
24
+ })
25
+
26
+ it(`capture outbound requests in e2e.calls`, async () => {
27
+ await e2e.sendText(`/start`, asUser)
28
+
29
+ const sendMessageReq = e2e.lastCall(`sendMessage`)
30
+ expect(sendMessageReq).toBeDefined()
31
+ expect(sendMessageReq?.body?.text).toContain(`Welcome`)
32
+ })
33
+ })
@@ -0,0 +1,44 @@
1
+ import {describe, it, expect, beforeEach} from "bun:test"
2
+ import {e2e} from "./setup"
3
+
4
+ const intent = `sum-number`
5
+ const asUser = {user: {id: 2004, username: `testuser`}, chat: {id: 2004}}
6
+
7
+ beforeEach(() => {
8
+ e2e.clearCalls()
9
+ })
10
+
11
+ describe(`E2E: SumNumberPage`, () => {
12
+ it(`complete two-step sum flow and show result`, async () => {
13
+ await e2e.sendText(`/start`, asUser)
14
+ e2e.clearCalls()
15
+
16
+ const step1 = await e2e.sendCallbackQuery(intent, asUser)
17
+ expect(step1.length).toBeGreaterThan(0)
18
+ expect(step1[0]!.body?.text).toBe(e2e.gmsg(intent, `s1`, {}))
19
+ e2e.clearCalls()
20
+
21
+ const step2 = await e2e.sendText(`23`, asUser)
22
+ expect(step2.length).toBeGreaterThan(0)
23
+ expect(step2[0]!.body?.text).toBe(e2e.gmsg(intent, `s2`, {}))
24
+ e2e.clearCalls()
25
+
26
+ const result = await e2e.sendText(`10`, asUser)
27
+ expect(result.length).toBeGreaterThan(0)
28
+ expect(result[0]!.body?.text).toContain(`Result Sum`)
29
+ expect(result[0]!.body?.text).toContain(`33`)
30
+ expect(e2e.extractButtonLabels(result[0])).toEqual(e2e.gkb(intent, `s`, `inline`, {}))
31
+ })
32
+
33
+ it(`re-prompt step 1 on invalid (non-numeric) input`, async () => {
34
+ await e2e.sendText(`/start`, asUser)
35
+ e2e.clearCalls()
36
+
37
+ await e2e.sendCallbackQuery(intent, asUser)
38
+ e2e.clearCalls()
39
+
40
+ const retry = await e2e.sendText(`not-a-number`, asUser)
41
+ expect(retry.length).toBeGreaterThan(0)
42
+ expect(retry[0]!.body?.text).toBe(e2e.gmsg(intent, `s1`, {}))
43
+ })
44
+ })
@@ -0,0 +1,24 @@
1
+ import {describe, it, expect, beforeEach} from "bun:test"
2
+ import {e2e} from "./setup"
3
+
4
+ const intent = `username-notfound`
5
+
6
+ beforeEach(() => {
7
+ e2e.clearCalls()
8
+ })
9
+
10
+ describe(`E2E: UserNameNotFoundPage`, () => {
11
+ it(`blocks user without username and shows username required page`, async () => {
12
+ const responses = await e2e.sendText(`/start`, {
13
+ user: {id: 77777, first_name: `NoUsername`},
14
+ chat: {id: 77777, type: `private`, first_name: `NoUsername`}
15
+ })
16
+
17
+ expect(responses.length).toBeGreaterThan(0)
18
+ const reply = responses[0]!
19
+ expect(reply).toBeDefined()
20
+ expect(reply.body?.text).toBe(e2e.gmsg(intent, `s1`, {}))
21
+ expect(reply.body?.text).toContain(`Username Required`)
22
+ expect(e2e.extractButtonLabels(reply)).toEqual(e2e.gkb(intent, `s1`, `inline`, {}))
23
+ })
24
+ })
@@ -38,7 +38,7 @@
38
38
  "test:all": "bun test test/ e2e/"
39
39
  },
40
40
  "dependencies": {
41
- "gramstax": "^0.8.23",
41
+ "gramstax": "^0.9.0",
42
42
  "dayjs": "^1.11.13"
43
43
  },
44
44
  "peerDependencies": {
@@ -12,7 +12,7 @@ export class BotCore extends Gramstax {
12
12
  return true
13
13
  }
14
14
 
15
- async hookErrorPage(ctx: CtxCore, data: Parameters<NonNullable<Gramstax[`hookErrorPage`]>>[1]) {
15
+ async hookErrorPage(ctx: CtxCore, data: NonNullable<Parameters<NonNullable<Gramstax[`hookErrorPage`]>>[1]>) {
16
16
  await new GeneralStatusPage(ctx).showError(`${data.listenerName}:hookErrorPage`, data.error, data.isEdit)
17
17
  }
18
18
 
@@ -5,5 +5,5 @@ export class BlankPage extends PageBase {
5
5
  await this.callbackQueryAnswer()
6
6
  }
7
7
 
8
- static template = `<base></base>`
8
+ static template = `<g-base></g-base>`
9
9
  }
@@ -54,18 +54,18 @@ export class GeneralStatusPage extends PageBase {
54
54
  }
55
55
 
56
56
  static template = `
57
- <base id="${Base.dynamic}">
57
+ <g-base id="${Base.dynamic}">
58
58
  ${this.kbk.home()}
59
- <message>
59
+ <g-message>
60
60
  <b>{~isNotFound ? "🔎 Route Not Found" : "❌ Error Occurred"~}</b>
61
61
 
62
62
  {~isNotFound ? "The requested route does not exist or is unavailable." : "Something went wrong while processing your request. Please try again."~}
63
- </message>
64
- <message lang="id">
63
+ </g-message>
64
+ <g-message lang="id">
65
65
  <b>{~isNotFound ? "🔎 Rute Tidak Ditemukan" : "❌ Terjadi Kesalahan"~}</b>
66
66
 
67
67
  {~isNotFound ? "Rute yang diminta tidak tersedia atau tidak ada." : "Terjadi kesalahan saat memproses permintaan. Silakan coba lagi."~}
68
- </message>
69
- </base>
68
+ </g-message>
69
+ </g-base>
70
70
  `
71
71
  }
@@ -32,24 +32,38 @@ export class HelpPage extends PageBase {
32
32
  }
33
33
 
34
34
  static template = `
35
- <base id="${Base.step1}">
35
+ <g-base id="${Base.step1}">
36
36
  ${this.kbk.back()}
37
- <message>
37
+ <g-message>
38
38
  <b>🆘 Help Message</b>
39
39
 
40
40
  Use this command to get help and instructions.
41
41
 
42
42
  <b>🎰 Random Number</b> - Generate a random number
43
43
  <b>➕ Sum Number</b> - Add two numbers together
44
- </message>
45
- <message lang="id">
44
+
45
+ <details><summary>📋 More commands</summary>
46
+ /start - Show welcome message
47
+ /help - Show this help
48
+ /random - Generate a random number
49
+ /sum - Add two numbers
50
+ </details>
51
+ </g-message>
52
+ <g-message lang="id">
46
53
  <b>🆘 Pesan Bantuan</b>
47
54
 
48
55
  Gunakan perintah ini untuk melihat bantuan dan petunjuk.
49
56
 
50
57
  <b>🎰 Angka Acak</b> - Menghasilkan angka secara acak
51
58
  <b>➕ Jumlah Angka</b> - Menjumlahkan dua angka
52
- </message>
53
- </base>
59
+
60
+ <details><summary>📋 Perintah lainnya</summary>
61
+ /start - Tampilkan pesan selamat datang
62
+ /help - Tampilkan bantuan ini
63
+ /random - Hasilkan angka acak
64
+ /sum - Jumlahkan dua angka
65
+ </details>
66
+ </g-message>
67
+ </g-base>
54
68
  `
55
69
  }
@@ -36,23 +36,23 @@ export class RandomNumberPage extends PageBase {
36
36
  }
37
37
 
38
38
  static template = `
39
- <base id="${Base.step1}">
40
- <keyboard>🎲 Shake, ${this.kbl.back.default}</keyboard>
41
- <keyboard lang="id">🎲 Kocok, ${this.kbl.back.id}</keyboard>
42
- <message>
39
+ <g-base id="${Base.step1}">
40
+ <g-keyboard>🎲 Shake, ${this.kbl.back.default}</g-keyboard>
41
+ <g-keyboard lang="id">🎲 Kocok, ${this.kbl.back.id}</g-keyboard>
42
+ <g-message>
43
43
  <b>🎰 Random Number</b>
44
44
 
45
45
  {{result}}
46
46
 
47
47
  Generated automatically based SCALE 18.
48
- </message>
49
- <message lang="id">
48
+ </g-message>
49
+ <g-message lang="id">
50
50
  <b>🎰 Angka Acak</b>
51
51
 
52
52
  {{result}}
53
53
 
54
54
  Dihasilkan secara otomatis berdasarkan SCALE 18.
55
- </message>
56
- </base>
55
+ </g-message>
56
+ </g-base>
57
57
  `
58
58
  }
@@ -39,10 +39,10 @@ export class StartPage extends PageBase {
39
39
  }
40
40
 
41
41
  static template = `
42
- <base id="${Base.step1}">
43
- <keyboard>🎰 Random Number, ➕ Sum Number, 🆘 Help</keyboard>
44
- <keyboard lang="id">🎰 Angka Acak, ➕ Jumlah Angka, 🆘 Bantuan</keyboard>
45
- <message>
42
+ <g-base id="${Base.step1}">
43
+ <g-keyboard>🎰 Random Number, ➕ Sum Number, 🆘 Help</g-keyboard>
44
+ <g-keyboard lang="id">🎰 Angka Acak, ➕ Jumlah Angka, 🆘 Bantuan</g-keyboard>
45
+ <g-message>
46
46
  <b>👋 Welcome to the bot {{userName}}</b>
47
47
 
48
48
  This bot was initially generated using Gramstax as the base setup and starting structure. It provides a clean foundation that can be extended with custom features, commands, and integrations as needed.
@@ -50,8 +50,12 @@ export class StartPage extends PageBase {
50
50
  <b>🎰 Random Number</b> - Generate a random number
51
51
  <b>➕ Sum Number</b> - Add two numbers together
52
52
  <b>❓ Help</b> - View help and available commands
53
- </message>
54
- <message lang="id">
53
+
54
+ <tg-spoiler>🎁 Hidden tip: try /help for a surprise</tg-spoiler>
55
+
56
+ <a href="https://github.com/gramstax/gramstax">📚 Documentation</a>
57
+ </g-message>
58
+ <g-message lang="id">
55
59
  <b>👋 Selamat datang di bot {{userName}}</b>
56
60
 
57
61
  Bot ini dibuat menggunakan Gramstax sebagai generator awal dan struktur dasar. Dari sini, bot dapat dikembangkan lebih lanjut dengan fitur, perintah, dan integrasi sesuai kebutuhan.
@@ -59,7 +63,11 @@ export class StartPage extends PageBase {
59
63
  <b>🎰 Angka Acak</b> - Menghasilkan angka secara acak
60
64
  <b>➕ Jumlah Angka</b> - Menjumlahkan dua angka
61
65
  <b>❓ Bantuan</b> - Lihat bantuan dan daftar perintah
62
- </message>
63
- </base>
66
+
67
+ <tg-spoiler>🎁 Tips tersembunyi: coba /help untuk kejutan</tg-spoiler>
68
+
69
+ <a href="https://github.com/gramstax/gramstax">📚 Dokumentasi</a>
70
+ </g-message>
71
+ </g-base>
64
72
  `
65
73
  }
@@ -12,8 +12,8 @@ class Helper {
12
12
  return (ctx.temp.session?.params as {step?: (typeof Base)[keyof typeof Base]; left?: number} | undefined) ?? {}
13
13
  }
14
14
 
15
- static kb(ctx: SumNumberPage, baseId: string) {
16
- return ctx.inlineKeyboardT([StartPage], `1`, baseId)
15
+ static kb(ctx: SumNumberPage, AGBaseId: string) {
16
+ return ctx.inlineKeyboardT([StartPage], `1`, AGBaseId)
17
17
  }
18
18
  }
19
19
 
@@ -86,46 +86,46 @@ export class SumNumberPage extends PageBase {
86
86
  }
87
87
 
88
88
  static template = `
89
- <base id="${Base.step1}">
89
+ <g-base id="${Base.step1}">
90
90
  ${this.kbk.cancel()}
91
- <message>
91
+ <g-message>
92
92
  <b>📚 Input Left Number</b>
93
93
 
94
94
  Example: 23
95
- </message>
96
- <message lang="id">
95
+ </g-message>
96
+ <g-message lang="id">
97
97
  <b>📚 Masukan Angka Kiri</b>
98
98
 
99
99
  Contoh: 23
100
- </message>
101
- </base>
100
+ </g-message>
101
+ </g-base>
102
102
 
103
- <base id="${Base.step2}">
103
+ <g-base id="${Base.step2}">
104
104
  ${this.kbk.cancel()}
105
- <message>
105
+ <g-message>
106
106
  <b>📚 Input Right Number</b>
107
107
 
108
108
  Example: 10
109
- </message>
110
- <message lang="id">
109
+ </g-message>
110
+ <g-message lang="id">
111
111
  <b>📚 Masukan Angka Kanan</b>
112
112
 
113
113
  Contoh: 10
114
- </message>
115
- </base>
114
+ </g-message>
115
+ </g-base>
116
116
 
117
- <base id="${Base.success}">
117
+ <g-base id="${Base.success}">
118
118
  ${this.kbk.home()}
119
- <message>
119
+ <g-message>
120
120
  <b>📚 Result Sum</b>
121
121
 
122
122
  {{result}}
123
- </message>
124
- <message lang="id">
123
+ </g-message>
124
+ <g-message lang="id">
125
125
  <b>📚 Hasil Sum</b>
126
126
 
127
127
  {{result}}
128
- </message>
129
- </base>
128
+ </g-message>
129
+ </g-base>
130
130
  `
131
131
  }
@@ -28,18 +28,18 @@ export class UserNameNotFoundPage extends PageBase {
28
28
  }
29
29
 
30
30
  static template = `
31
- <base id="${Base.step1}">
31
+ <g-base id="${Base.step1}">
32
32
  ${this.kbk.home()}
33
- <message>
33
+ <g-message>
34
34
  <b>⚠️ Username Required</b>
35
35
 
36
36
  Please set the username first before using the bot.
37
- </message>
38
- <message lang="id">
37
+ </g-message>
38
+ <g-message lang="id">
39
39
  <b>⚠️ Username Diperlukan</b>
40
40
 
41
41
  Tolong set username dulu sebelum memakai bot.
42
- </message>
43
- </base>
42
+ </g-message>
43
+ </g-base>
44
44
  `
45
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-gramstax",
3
- "version": "0.8.24",
3
+ "version": "0.10.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -1,287 +0,0 @@
1
- /**
2
- * E2E Test Flow for Template Bot
3
- *
4
- * Setup:
5
- * 1. MockTelegramServer starts on random port → apiRoot captured
6
- * 2. BotCore initialized with webhook:http://127.0.0.1:3009/webhook/e2e-template
7
- * 3. CacheExternal (memory) shared across tests, cleared in beforeEach
8
- *
9
- * Test Flow:
10
- * T1. /start → Welcome message + inline keyboard (Random Number, Sum Number, Help)
11
- * T2. /start → callback:help → editMessage → Help Message + Back button
12
- * T3. /start → callback:random-number → editMessage → Random Number + Shake/Back buttons
13
- * T4. /start → callback:random-number → callback:random-number → editMessage → refreshed number
14
- * T5. /start → callback:sum-number → text:23 → text:10 → Result Sum: 33 + Home button
15
- * T6. /start → callback:sum-number → text:not-a-number → re-prompt Input Left Number
16
- * T7. /start → callback:nonexistent_action → graceful handling (no crash)
17
- * T8. /start → verify sendMessage request captured in mockServer.requests
18
- * T9. /start → callback:help → callback:random-number → sequential chain
19
- * T10. sendPhoto → Route Not Found (no handler for photo)
20
- * T11. /start with custom user/chat → custom ID propagation verified
21
- * T12. /start without username → Username Required page (UserGuard blocks)
22
- * T13. /start → callback:help → callback:random-number → keyboard structure validation (text + callback_data)
23
- *
24
- * Cleanup:
25
- * - botCore.stop() + mockServer.stop() in afterAll
26
- * - cacheSession.clear() + mockServer.clearRequests() in beforeEach
27
- */
28
- import {join} from "node:path"
29
- import {BotCore} from "../src/core/bot"
30
- import {TemplateManager, CacheExternal} from "gramstax"
31
- import {MockTelegramServer} from "gramstax"
32
- import {describe, it, expect, beforeAll, afterAll, beforeEach} from "bun:test"
33
-
34
- let mockServer: MockTelegramServer
35
- let botCore: BotCore
36
- let cacheSession: CacheExternal
37
-
38
- const USER_1 = {
39
- responseTimeoutMs: undefined, // 9000,
40
- user: {
41
- id: 123456789,
42
- first_name: `Test`,
43
- last_name: `user`,
44
- username: `testuser`,
45
- language_code: `en`,
46
- is_bot: false
47
- }
48
- }
49
-
50
- beforeAll(async () => {
51
- mockServer = new MockTelegramServer({log: false})
52
- await mockServer.start()
53
-
54
- const templateManager = new TemplateManager({path: [join(import.meta.dir, `../src/pages`)]})
55
- const cwd = process.cwd()
56
- process.chdir(join(import.meta.dir, `..`))
57
-
58
- cacheSession = new CacheExternal(`memory`)
59
-
60
- botCore = new BotCore({
61
- deploy: `webhook:http://127.0.0.1:3009/webhook/e2e-template`,
62
- token: `test-token`,
63
- log: false,
64
- templateManager,
65
- cacheSession,
66
- botConfig: {client: {apiRoot: mockServer.apiRoot}}
67
- })
68
-
69
- process.chdir(cwd)
70
-
71
- const startedAt = Date.now()
72
- while (!mockServer.webhookUrl && Date.now() - startedAt < 5000) {
73
- await new Promise((r) => setTimeout(r, 50))
74
- }
75
- })
76
-
77
- afterAll(async () => {
78
- await botCore.stop()
79
- await mockServer.stop()
80
- })
81
-
82
- beforeEach(async () => {
83
- mockServer.clearRequests()
84
- await cacheSession.clear()
85
- })
86
-
87
- describe(`E2E: Template bot via MockTelegramServer (webhook)`, () => {
88
- it(`responds to /start command with welcome message and navigation buttons`, async () => {
89
- const responses = await mockServer.sendText(`/start`, USER_1)
90
-
91
- expect(responses.length).toBeGreaterThan(0)
92
- const reply = responses[0]!
93
- expect(reply).toBeDefined()
94
- expect(reply!.body?.text).toContain(`Welcome to the bot`)
95
-
96
- const keyboard = reply!.body?.reply_markup?.inline_keyboard
97
- expect(Array.isArray(keyboard)).toBe(true)
98
- expect(keyboard.length).toBeGreaterThan(0)
99
-
100
- const allButtons = keyboard.flat()
101
- const buttonLabels = allButtons.map((b: any) => b.text)
102
- expect(buttonLabels).toContain(`🎰 Random Number`)
103
- expect(buttonLabels).toContain(`➕ Sum Number`)
104
- expect(buttonLabels).toContain(`🆘 Help`)
105
- })
106
-
107
- it(`navigates to Help page via callback and shows back button`, async () => {
108
- await mockServer.sendText(`/start`, USER_1)
109
- mockServer.clearRequests()
110
-
111
- const responses = await mockServer.sendCallbackQuery(`help`, USER_1)
112
-
113
- expect(responses.length).toBeGreaterThan(0)
114
- const edit = responses[0]!
115
- expect(edit!.body?.text).toContain(`Help Message`)
116
-
117
- const keyboard = edit!.body?.reply_markup?.inline_keyboard
118
- expect(Array.isArray(keyboard)).toBe(true)
119
- const firstButton = keyboard[0]?.[0]
120
- expect(firstButton?.text).toContain(`Back`)
121
- })
122
-
123
- it(`navigates to Random Number page via callback and shows generated number`, async () => {
124
- await mockServer.sendText(`/start`, USER_1)
125
- mockServer.clearRequests()
126
-
127
- const responses = await mockServer.sendCallbackQuery(`random-number`, USER_1)
128
-
129
- expect(responses.length).toBeGreaterThan(0)
130
- const edit = responses[0]!
131
- expect(edit!.body?.text).toContain(`Random Number`)
132
-
133
- const keyboard = edit!.body?.reply_markup?.inline_keyboard
134
- expect(Array.isArray(keyboard)).toBe(true)
135
- const allButtons = keyboard.flat()
136
- const buttonLabels = allButtons.map((b: any) => b.text)
137
- expect(buttonLabels).toContain(`🎲 Shake`)
138
- expect(buttonLabels).toContain(`⬅️ Back`)
139
- })
140
-
141
- it(`refreshes random number via Shake callback`, async () => {
142
- await mockServer.sendText(`/start`, USER_1)
143
- mockServer.clearRequests()
144
-
145
- await mockServer.sendCallbackQuery(`random-number`, USER_1)
146
- mockServer.clearRequests()
147
-
148
- const responses = await mockServer.sendCallbackQuery(`random-number`, USER_1)
149
-
150
- expect(responses.length).toBeGreaterThan(0)
151
- const edit = responses[0]!
152
- expect(edit!.body?.text).toContain(`Random Number`)
153
- })
154
-
155
- it(`completes Sum Number two-step session flow`, async () => {
156
- await mockServer.sendText(`/start`, USER_1)
157
- mockServer.clearRequests()
158
-
159
- const sumResponses = await mockServer.sendCallbackQuery(`sum-number`, USER_1)
160
- expect(sumResponses.length).toBeGreaterThan(0)
161
- expect(sumResponses[0]!.body?.text).toContain(`Input Left Number`)
162
-
163
- mockServer.clearRequests()
164
-
165
- const leftResponses = await mockServer.sendText(`23`, USER_1)
166
- expect(leftResponses.length).toBeGreaterThan(0)
167
- expect(leftResponses[0]!.body?.text).toContain(`Input Right Number`)
168
-
169
- mockServer.clearRequests()
170
-
171
- const resultResponses = await mockServer.sendText(`10`, USER_1)
172
- expect(resultResponses.length).toBeGreaterThan(0)
173
- expect(resultResponses[0]!.body?.text).toContain(`Result Sum`)
174
- expect(resultResponses[0]!.body?.text).toContain(`33`)
175
-
176
- const keyboard = resultResponses[0]!.body?.reply_markup?.inline_keyboard
177
- expect(Array.isArray(keyboard)).toBe(true)
178
- const firstButton = keyboard[0]?.[0]
179
- expect(firstButton?.text).toContain(`Home`)
180
- })
181
-
182
- it(`handles invalid input in Sum Number gracefully`, async () => {
183
- await mockServer.sendText(`/start`, USER_1)
184
- mockServer.clearRequests()
185
-
186
- await mockServer.sendCallbackQuery(`sum-number`, USER_1)
187
- mockServer.clearRequests()
188
-
189
- const leftResponses = await mockServer.sendText(`not-a-number`, USER_1)
190
- expect(leftResponses.length).toBeGreaterThan(0)
191
- expect(leftResponses[0]!.body?.text).toContain(`Input Left Number`)
192
- })
193
-
194
- it(`shows route not found for unrecognized callback`, async () => {
195
- await mockServer.sendText(`/start`, USER_1)
196
- mockServer.clearRequests()
197
-
198
- const responses = await mockServer.sendCallbackQuery(`nonexistent_action_${Math.random()}`, USER_1)
199
- expect(responses.length).toBeGreaterThanOrEqual(0)
200
- })
201
-
202
- it(`captures outbound requests in mockServer.requests`, async () => {
203
- const responses = await mockServer.sendText(`/start`, USER_1)
204
-
205
- expect(responses.length).toBeGreaterThan(0)
206
- const sendMessageReq = mockServer.lastRequest(`sendMessage`)
207
- expect(sendMessageReq).toBeDefined()
208
- expect(sendMessageReq?.body?.text).toContain(`Welcome to the bot`)
209
- })
210
-
211
- it(`handles multiple sequential interactions`, async () => {
212
- const res1 = await mockServer.sendText(`/start`, USER_1)
213
- expect(res1[0]?.body?.text).toContain(`Welcome to the bot`)
214
-
215
- mockServer.clearRequests()
216
-
217
- const res2 = await mockServer.sendCallbackQuery(`help`, USER_1)
218
- expect(res2[0]?.body?.text).toContain(`Help Message`)
219
-
220
- mockServer.clearRequests()
221
-
222
- const res3 = await mockServer.sendCallbackQuery(`random-number`, USER_1)
223
- expect(res3[0]?.body?.text).toContain(`Random Number`)
224
- })
225
-
226
- it(`sendPhoto triggers a response`, async () => {
227
- const responses = await mockServer.sendPhoto({caption: `test photo`, ...USER_1})
228
-
229
- expect(responses.length).toBeGreaterThan(0)
230
- expect(responses[0]?.body?.text).toContain(`Route Not Found`)
231
- })
232
-
233
- it(`supports custom user and chat options`, async () => {
234
- const responses = await mockServer.sendText(`/start`, {
235
- user: {id: 99999, first_name: `CustomUser`, username: `customuser`},
236
- chat: {id: 88888, type: `private`, first_name: `CustomChat`}
237
- })
238
-
239
- expect(responses.length).toBeGreaterThan(0)
240
- expect(responses[0]?.body?.text).toContain(`Welcome to the bot`)
241
-
242
- const lastUpdate = mockServer.updates[mockServer.updates.length - 1]
243
- expect(lastUpdate).toBeDefined()
244
- const msg = (lastUpdate as any)?.message
245
- expect(msg?.from?.id).toBe(99999)
246
- expect(msg?.chat?.id).toBe(88888)
247
- })
248
-
249
- it(`blocks user without username and shows username required page`, async () => {
250
- const responses = await mockServer.sendText(`/start`, {
251
- user: {id: 77777, first_name: `NoUsername`},
252
- chat: {id: 77777, type: `private`, first_name: `NoUsername`}
253
- })
254
-
255
- expect(responses.length).toBeGreaterThan(0)
256
- const reply = responses[0]!
257
- expect(reply.body?.text).toContain(`Username Required`)
258
- expect(reply.body?.text).toContain(`set the username first`)
259
- })
260
-
261
- it(`validates inline_keyboard structure for all navigation pages`, async () => {
262
- const res1 = await mockServer.sendText(`/start`, USER_1)
263
- expect(Array.isArray(res1[0]?.body?.reply_markup?.inline_keyboard)).toBe(true)
264
-
265
- mockServer.clearRequests()
266
-
267
- const res2 = await mockServer.sendCallbackQuery(`help`, USER_1)
268
- const kb2 = res2[0]?.body?.reply_markup?.inline_keyboard
269
- expect(Array.isArray(kb2)).toBe(true)
270
- expect(kb2!.length).toBeGreaterThan(0)
271
- expect(kb2![0]?.[0]?.text).toBeDefined()
272
- expect(kb2![0]?.[0]?.callback_data).toBeDefined()
273
-
274
- mockServer.clearRequests()
275
-
276
- const res3 = await mockServer.sendCallbackQuery(`random-number`, USER_1)
277
- const kb3 = res3[0]?.body?.reply_markup?.inline_keyboard
278
- expect(Array.isArray(kb3)).toBe(true)
279
- expect(kb3!.length).toBeGreaterThan(0)
280
- for (const row of kb3!) {
281
- for (const btn of row) {
282
- expect(btn?.text).toBeDefined()
283
- expect(btn?.callback_data).toBeDefined()
284
- }
285
- }
286
- })
287
- })